From 832d51a0558d45a02ddd022eda1bf81df179edee Mon Sep 17 00:00:00 2001 From: Dharanish Date: Tue, 28 May 2024 19:39:53 +0200 Subject: [PATCH] Make HabitGroups simpler No subgroups --- .../habits/edit/EditHabitGroupActivity.kt | 232 ++++++++++++++++++ .../isoron/uhabits/intents/IntentFactory.kt | 12 + .../src/main/res/layout/show_habit_group.xml | 72 ++++++ .../core/commands/CreateHabitGroupCommand.kt | 18 ++ .../core/commands/EditHabitGroupCommand.kt | 20 ++ .../isoron/uhabits/core/models/HabitGroup.kt | 33 +-- .../uhabits/core/models/HabitGroupList.kt | 25 -- .../uhabits/core/models/ModelFactory.kt | 2 - .../isoron/uhabits/core/models/ScoreList.kt | 4 +- .../isoron/uhabits/core/models/StreakList.kt | 5 +- .../models/memory/MemoryHabitGroupList.kt | 4 - .../models/sqlite/records/HabitGroupRecord.kt | 10 - .../src/jvmMain/resources/migrations/26.sql | 4 +- 13 files changed, 361 insertions(+), 80 deletions(-) create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitGroupActivity.kt create mode 100644 uhabits-android/src/main/res/layout/show_habit_group.xml create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/CreateHabitGroupCommand.kt create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/EditHabitGroupCommand.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitGroupActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitGroupActivity.kt new file mode 100644 index 000000000..50fc5ef78 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitGroupActivity.kt @@ -0,0 +1,232 @@ +package org.isoron.uhabits.activities.habits.edit + +import android.content.res.ColorStateList +import android.graphics.Color +import android.os.Bundle +import android.text.Html +import android.text.Spanned +import android.text.format.DateFormat +import android.view.View +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.DialogFragment +import com.android.datetimepicker.time.RadialPickerLayout +import com.android.datetimepicker.time.TimePickerDialog +import org.isoron.platform.gui.toInt +import org.isoron.uhabits.HabitsApplication +import org.isoron.uhabits.R +import org.isoron.uhabits.activities.AndroidThemeSwitcher +import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory +import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog +import org.isoron.uhabits.core.commands.CommandRunner +import org.isoron.uhabits.core.commands.CreateHabitGroupCommand +import org.isoron.uhabits.core.commands.EditHabitGroupCommand +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.PaletteColor +import org.isoron.uhabits.core.models.Reminder +import org.isoron.uhabits.core.models.WeekdayList +import org.isoron.uhabits.databinding.ActivityEditHabitGroupBinding +import org.isoron.uhabits.utils.ColorUtils +import org.isoron.uhabits.utils.dismissCurrentAndShow +import org.isoron.uhabits.utils.formatTime +import org.isoron.uhabits.utils.toFormattedString + +class EditHabitGroupActivity : AppCompatActivity() { + + private lateinit var themeSwitcher: AndroidThemeSwitcher + private lateinit var binding: ActivityEditHabitGroupBinding + private lateinit var commandRunner: CommandRunner + + var habitGroupId = -1L + var color = PaletteColor(11) + var androidColor = 0 + var reminderHour = -1 + var reminderMin = -1 + var reminderDays: WeekdayList = WeekdayList.EVERY_DAY + + override fun onCreate(state: Bundle?) { + super.onCreate(state) + + val component = (application as HabitsApplication).component + themeSwitcher = AndroidThemeSwitcher(this, component.preferences) + themeSwitcher.apply() + + binding = ActivityEditHabitGroupBinding.inflate(layoutInflater) + setContentView(binding.root) + + if (intent.hasExtra("habitGroupId")) { + binding.toolbar.title = getString(R.string.edit_habit_group) + habitGroupId = intent.getLongExtra("habitId", -1) + val hgr = component.habitGroupList.getById(habitGroupId)!! + color = hgr.color + hgr.reminder?.let { + reminderHour = it.hour + reminderMin = it.minute + reminderDays = it.days + } + binding.nameInput.setText(hgr.name) + binding.questionInput.setText(hgr.question) + binding.notesInput.setText(hgr.description) + } + + if (state != null) { + habitGroupId = state.getLong("habitGroupId") + color = PaletteColor(state.getInt("paletteColor")) + reminderHour = state.getInt("reminderHour") + reminderMin = state.getInt("reminderMin") + reminderDays = WeekdayList(state.getInt("reminderDays")) + } + + updateColors() + + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.elevation = 10.0f + + val colorPickerDialogFactory = ColorPickerDialogFactory(this) + binding.colorButton.setOnClickListener { + val picker = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme) + picker.setListener { paletteColor -> + this.color = paletteColor + updateColors() + } + picker.dismissCurrentAndShow(supportFragmentManager, "colorPicker") + } + + populateReminder() + binding.reminderTimePicker.setOnClickListener { + val currentHour = if (reminderHour >= 0) reminderHour else 8 + val currentMin = if (reminderMin >= 0) reminderMin else 0 + val is24HourMode = DateFormat.is24HourFormat(this) + val dialog = TimePickerDialog.newInstance( + object : TimePickerDialog.OnTimeSetListener { + override fun onTimeSet(view: RadialPickerLayout?, hourOfDay: Int, minute: Int) { + reminderHour = hourOfDay + reminderMin = minute + populateReminder() + } + + override fun onTimeCleared(view: RadialPickerLayout?) { + reminderHour = -1 + reminderMin = -1 + reminderDays = WeekdayList.EVERY_DAY + populateReminder() + } + }, + currentHour, + currentMin, + is24HourMode, + androidColor + ) + dialog.dismissCurrentAndShow(supportFragmentManager, "timePicker") + } + + binding.reminderDatePicker.setOnClickListener { + val dialog = WeekdayPickerDialog() + + dialog.setListener { days: WeekdayList -> + reminderDays = days + if (reminderDays.isEmpty) reminderDays = WeekdayList.EVERY_DAY + populateReminder() + } + dialog.setSelectedDays(reminderDays) + dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker") + } + + binding.buttonSave.setOnClickListener { + if (validate()) save() + } + + for (fragment in supportFragmentManager.fragments) { + (fragment as DialogFragment).dismiss() + } + } + + private fun save() { + val component = (application as HabitsApplication).component + val hgr = component.modelFactory.buildHabitGroup() + + var original: HabitGroup? = null + if (habitGroupId >= 0) { + original = component.habitGroupList.getById(habitGroupId)!! + hgr.copyFrom(original) + } + + hgr.name = binding.nameInput.text.trim().toString() + hgr.question = binding.questionInput.text.trim().toString() + hgr.description = binding.notesInput.text.trim().toString() + hgr.color = color + if (reminderHour >= 0) { + hgr.reminder = Reminder(reminderHour, reminderMin, reminderDays) + } else { + hgr.reminder = null + } + + val command = if (habitGroupId >= 0) { + EditHabitGroupCommand( + component.habitGroupList, + habitGroupId, + hgr + ) + } else { + CreateHabitGroupCommand( + component.modelFactory, + component.habitGroupList, + hgr + ) + } + component.commandRunner.run(command) + finish() + } + + private fun validate(): Boolean { + var isValid = true + if (binding.nameInput.text.isEmpty()) { + binding.nameInput.error = getFormattedValidationError(R.string.validation_cannot_be_blank) + isValid = false + } + return isValid + } + + private fun populateReminder() { + if (reminderHour < 0) { + binding.reminderTimePicker.text = getString(R.string.reminder_off) + binding.reminderDatePicker.visibility = View.GONE + binding.reminderDivider.visibility = View.GONE + } else { + val time = formatTime(this, reminderHour, reminderMin) + binding.reminderTimePicker.text = time + binding.reminderDatePicker.visibility = View.VISIBLE + binding.reminderDivider.visibility = View.VISIBLE + binding.reminderDatePicker.text = reminderDays.toFormattedString(this) + } + } + + private fun updateColors() { + androidColor = themeSwitcher.currentTheme.color(color).toInt() + binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor) + if (!themeSwitcher.isNightMode) { + val darkerAndroidColor = ColorUtils.mixColors(Color.BLACK, androidColor, 0.15f) + window.statusBarColor = darkerAndroidColor + binding.toolbar.setBackgroundColor(androidColor) + } + } + + private fun getFormattedValidationError(@StringRes resId: Int): Spanned { + val html = "${getString(resId)}" + return Html.fromHtml(html) + } + + override fun onSaveInstanceState(state: Bundle) { + super.onSaveInstanceState(state) + with(state) { + putLong("habitGroupId", habitGroupId) + putInt("paletteColor", color.paletteIndex) + putInt("androidColor", androidColor) + putInt("reminderHour", reminderHour) + putInt("reminderMin", reminderMin) + putInt("reminderDays", reminderDays.toInteger()) + } + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentFactory.kt b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentFactory.kt index 71b2938e7..33fa31aa3 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentFactory.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentFactory.kt @@ -25,10 +25,12 @@ import android.net.Uri import org.isoron.uhabits.R import org.isoron.uhabits.activities.about.AboutActivity import org.isoron.uhabits.activities.habits.edit.EditHabitActivity +import org.isoron.uhabits.activities.habits.edit.EditHabitGroupActivity import org.isoron.uhabits.activities.habits.show.ShowHabitActivity import org.isoron.uhabits.activities.intro.IntroActivity import org.isoron.uhabits.activities.settings.SettingsActivity import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import javax.inject.Inject class IntentFactory @@ -100,4 +102,14 @@ class IntentFactory intent.putExtra("habitType", habitType) return intent } + + fun startEditGroupActivity(context: Context): Intent { + return Intent(context, EditHabitGroupActivity::class.java) + } + + fun startEditGroupActivity(context: Context, habitGroup: HabitGroup): Intent { + val intent = startEditGroupActivity(context) + intent.putExtra("habitGroupId", habitGroup.id) + return intent + } } diff --git a/uhabits-android/src/main/res/layout/show_habit_group.xml b/uhabits-android/src/main/res/layout/show_habit_group.xml new file mode 100644 index 000000000..b8b433577 --- /dev/null +++ b/uhabits-android/src/main/res/layout/show_habit_group.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/CreateHabitGroupCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/CreateHabitGroupCommand.kt new file mode 100644 index 000000000..29675ca5c --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/CreateHabitGroupCommand.kt @@ -0,0 +1,18 @@ +package org.isoron.uhabits.core.commands + +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.HabitGroupList +import org.isoron.uhabits.core.models.ModelFactory + +data class CreateHabitGroupCommand( + val modelFactory: ModelFactory, + val habitGroupList: HabitGroupList, + val model: HabitGroup +) : Command { + override fun run() { + val habitGroup = modelFactory.buildHabitGroup() + habitGroup.copyFrom(model) + habitGroupList.add(habitGroup) + habitGroup.recompute() + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/EditHabitGroupCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/EditHabitGroupCommand.kt new file mode 100644 index 000000000..aa7ab6243 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/EditHabitGroupCommand.kt @@ -0,0 +1,20 @@ +package org.isoron.uhabits.core.commands + +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.HabitGroupList +import org.isoron.uhabits.core.models.HabitNotFoundException + +data class EditHabitGroupCommand( + val habitGroupList: HabitGroupList, + val habitGroupId: Long, + val modified: HabitGroup +) : Command { + override fun run() { + val habitGroup = habitGroupList.getById(habitGroupId) ?: throw HabitNotFoundException() + habitGroup.copyFrom(modified) + habitGroupList.update(habitGroup) + habitGroup.observable.notifyListeners() + habitGroup.recompute() + habitGroupList.resort() + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroup.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroup.kt index 5b80f1bc7..4526e1872 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroup.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroup.kt @@ -14,7 +14,6 @@ data class HabitGroup( var reminder: Reminder? = null, var uuid: String? = null, var habitList: HabitList, - var habitGroupList: HabitGroupList, val scores: ScoreList, val streaks: StreakList, var parentID: Long? = null, @@ -33,11 +32,11 @@ data class HabitGroup( fun hasReminder(): Boolean = reminder != null fun isCompletedToday(): Boolean { - return habitList.all { it.isCompletedToday() } && habitGroupList.all { it.isCompletedToday() } + return habitList.all { it.isCompletedToday() } } fun isEnteredToday(): Boolean { - return habitList.all { it.isEnteredToday() } && habitGroupList.all { it.isEnteredToday() } + return habitList.all { it.isEnteredToday() } } fun firstEntryDate(): Timestamp { @@ -47,16 +46,11 @@ data class HabitGroup( val first = h.firstEntryDate() if (earliest.isNewerThan(first)) earliest = first } - for (hgr in habitGroupList) { - val first = hgr.firstEntryDate() - if (earliest.isNewerThan(first)) earliest = first - } return earliest } fun recompute() { for (h in habitList) h.recompute() - for (hgr in habitGroupList) hgr.recompute() val today = DateUtils.getTodayWithOffset() val to = today.plus(30) @@ -65,14 +59,12 @@ data class HabitGroup( scores.combineFrom( habitList = habitList, - habitGroupList = habitGroupList, from = from, to = to ) streaks.combineFrom( habitList = habitList, - habitGroupList = habitGroupList, from = from, to = to ) @@ -134,23 +126,6 @@ data class HabitGroup( } } - fun getHabitByUUIDDeep(uuid: String?): Habit? { - val habit = habitList.getByUUID(uuid) - if (habit != null) return habit - for (hgr in habitGroupList) { - val found = hgr.getHabitByUUIDDeep(uuid) - if (found != null) return found - } - return null - } - - fun getHabitGroupByUUIDDeep(uuid: String?): HabitGroup? { - val habitGroup = habitGroupList.getByUUID(uuid) - if (habitGroup != null) return habitGroup - for (hgr in habitGroupList) { - val found = hgr.getHabitGroupByUUIDDeep(uuid) - if (found != null) return found - } - return null - } + fun getHabitByUUIDDeep(uuid: String?): Habit? = + habitList.getByUUID(uuid) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroupList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroupList.kt index 944adfbf3..f597eb1f7 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroupList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroupList.kt @@ -78,16 +78,6 @@ abstract class HabitGroupList : Iterable { return null } - fun getHabitGroupByUUIDDeep(uuid: String?): HabitGroup? { - for (hgr in this) { - val habit = hgr.getHabitGroupByUUIDDeep(uuid) - if (habit != null) { - return habit - } - } - return null - } - /** * Returns the habit that occupies a certain position. * @@ -191,21 +181,6 @@ abstract class HabitGroupList : Iterable { habitList.remove(h) } } - toRemove.clear() - for (hgr1 in this) { - val hgr2 = getByUUID(hgr1.parentUUID) - if (hgr2 != null) { - hgr2.habitGroupList.add(hgr1) - toRemove.add(hgr1.uuid) - hgr1.parent = hgr2 - } - } - for (uuid in toRemove) { - val h = getByUUID(uuid) - if (h != null) { - remove(h) - } - } for (hgr in this) { hgr.recompute() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelFactory.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelFactory.kt index b150aa03a..6287f6602 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelFactory.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelFactory.kt @@ -41,12 +41,10 @@ interface ModelFactory { } fun buildHabitGroup(): HabitGroup { val habits = buildHabitList() - val groups = buildHabitGroupList() val scores = buildScoreList() val streaks = buildStreakList() return HabitGroup( habitList = habits, - habitGroupList = groups, scores = scores, streaks = streaks ) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt index b608334b0..dc99c0677 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt @@ -141,15 +141,13 @@ class ScoreList { @Synchronized fun combineFrom( habitList: HabitList, - habitGroupList: HabitGroupList, from: Timestamp, to: Timestamp ) { var current = to while (current >= from) { val habitScores = habitList.map { it.scores[current].value } - val groupScores = habitGroupList.map { it.scores[current].value } - val averageScore = (habitScores + groupScores).average() + val averageScore = habitScores.average() map[current] = Score(current, averageScore) current = current.minus(1) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt index 466e7b596..494209fce 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt @@ -71,7 +71,6 @@ class StreakList { @Synchronized fun combineFrom( habitList: HabitList, - habitGroupList: HabitGroupList, from: Timestamp, to: Timestamp ) { @@ -79,9 +78,7 @@ class StreakList { var streakRunning = false var streakStart = from while (current <= to) { - if (habitList.all { it.streaks.isInStreaks(current) } && - habitGroupList.all { it.streaks.isInStreaks(current) } && - !streakRunning + if (habitList.all { it.streaks.isInStreaks(current) } && !streakRunning ) { streakStart = current streakRunning = true diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitGroupList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitGroupList.kt index 9502fdb28..bc80617ae 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitGroupList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitGroupList.kt @@ -205,10 +205,6 @@ class MemoryHabitGroupList : HabitGroupList { hgr.habitList.primaryOrder = primaryOrder hgr.habitList.secondaryOrder = secondaryOrder hgr.habitList.resort() - - hgr.habitGroupList.primaryOrder = primaryOrder - hgr.habitGroupList.secondaryOrder = secondaryOrder - hgr.habitGroupList.resort() } if (comparator != null) list.sortWith(comparator!!) observable.notifyListeners() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitGroupRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitGroupRecord.kt index 0e5a90699..0b34be917 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitGroupRecord.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitGroupRecord.kt @@ -49,12 +49,6 @@ class HabitGroupRecord { @field:Column var uuid: String? = null - @field:Column(name = "parent_id") - var parentID: Long? = null - - @field:Column(name = "parent_uuid") - var parentUUID: String? = null - fun copyFrom(model: HabitGroup) { id = model.id name = model.name @@ -68,8 +62,6 @@ class HabitGroupRecord { reminderDays = 0 reminderMin = null reminderHour = null - parentID = model.parentID - parentUUID = model.parentUUID if (model.hasReminder()) { val reminder = model.reminder reminderHour = requireNonNull(reminder)!!.hour @@ -87,8 +79,6 @@ class HabitGroupRecord { habitGroup.isArchived = archived != 0 habitGroup.position = position!! habitGroup.uuid = uuid - habitGroup.parentID = parentID - habitGroup.parentUUID = parentUUID if (reminderHour != null && reminderMin != null) { habitGroup.reminder = Reminder( reminderHour!!, diff --git a/uhabits-core/src/jvmMain/resources/migrations/26.sql b/uhabits-core/src/jvmMain/resources/migrations/26.sql index d934bd4b8..067a6bb1f 100644 --- a/uhabits-core/src/jvmMain/resources/migrations/26.sql +++ b/uhabits-core/src/jvmMain/resources/migrations/26.sql @@ -15,7 +15,5 @@ create table HabitGroups ( reminder_hour integer, reminder_min integer, question text not null default "", - uuid text, - parent_id integer, - parent_uuid integer + uuid text ); \ No newline at end of file