From 9bfae974eba59986ce9491229504374cd6217854 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sun, 12 May 2024 18:31:39 +0200 Subject: [PATCH 01/51] Upgrade gradle version --- build.gradle.kts | 2 +- gradle.properties | 1 + gradle/wrapper/gradle-wrapper.properties | 2 +- uhabits-android/build.gradle.kts | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b4e6235bf..c5279d865 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { val kotlinVersion = "1.9.22" - id("com.android.application") version "8.1.4" apply (false) + id("com.android.application") version "8.4.0" apply (false) id("org.jetbrains.kotlin.android") version kotlinVersion apply (false) id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false) id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false) diff --git a/gradle.properties b/gradle.properties index cfc7ec4b1..2035e4f83 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,3 +6,4 @@ android.enableJetifier=true android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false +org.gradle.java.installations.auto-download=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586a5..17655d0ef 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/uhabits-android/build.gradle.kts b/uhabits-android/build.gradle.kts index 28154d587..3b9e11bea 100644 --- a/uhabits-android/build.gradle.kts +++ b/uhabits-android/build.gradle.kts @@ -19,7 +19,7 @@ plugins { id("com.github.triplet.play") version "3.8.6" - id("com.android.application") version "8.1.4" + id("com.android.application") version "8.4.0" id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.kapt") id("org.jlleitschuh.gradle.ktlint") From ec08b602f373a6e8b63294fcaefd65b21a988e31 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sat, 18 May 2024 13:28:39 +0200 Subject: [PATCH 02/51] Skip days implemented. Scores not correct yet --- .../habits/edit/EditHabitActivity.kt | 37 +++++++++++++++++++ .../main/res/layout/activity_edit_habit.xml | 19 ++++++++++ .../src/main/res/values/strings.xml | 2 + .../assets/main/migrations/009.sql | 2 +- .../isoron/uhabits/models/HabitRepository.kt | 36 ++++++++++-------- .../isoron/uhabits/core/models/EntryList.kt | 11 +++--- .../isoron/uhabits/core/models/Frequency.kt | 2 +- .../org/isoron/uhabits/core/models/Habit.kt | 9 +++++ .../isoron/uhabits/core/models/ScoreList.kt | 4 +- .../isoron/uhabits/core/models/WeekdayList.kt | 12 ++++++ .../core/models/sqlite/SQLiteEntryList.kt | 10 ++--- .../core/models/sqlite/records/HabitRecord.kt | 10 +++++ .../screens/habits/list/HabitCardListCache.kt | 4 +- .../screens/habits/list/ListHabitsBehavior.kt | 3 +- .../src/jvmMain/resources/migrations/09.sql | 2 + 15 files changed, 132 insertions(+), 31 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt index a2f329d71..ab2371afc 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt @@ -81,6 +81,8 @@ class EditHabitActivity : AppCompatActivity() { var androidColor = 0 var freqNum = 1 var freqDen = 1 + var isSkipDays = false + var listSkipDays: WeekdayList = WeekdayList.NO_DAY var reminderHour = -1 var reminderMin = -1 var reminderDays: WeekdayList = WeekdayList.EVERY_DAY @@ -104,6 +106,8 @@ class EditHabitActivity : AppCompatActivity() { color = habit.color freqNum = habit.frequency.numerator freqDen = habit.frequency.denominator + isSkipDays = habit.skipDays + listSkipDays = habit.skipDaysList targetType = habit.targetType habit.reminder?.let { reminderHour = it.hour @@ -125,6 +129,8 @@ class EditHabitActivity : AppCompatActivity() { color = PaletteColor(state.getInt("paletteColor")) freqNum = state.getInt("freqNum") freqDen = state.getInt("freqDen") + isSkipDays = state.getBoolean("isSkipDays", false) + listSkipDays = WeekdayList(state.getInt("listSkipDays", 0)) reminderHour = state.getInt("reminderHour") reminderMin = state.getInt("reminderMin") reminderDays = WeekdayList(state.getInt("reminderDays")) @@ -241,12 +247,31 @@ class EditHabitActivity : AppCompatActivity() { dialog.setListener { days: WeekdayList -> reminderDays = days if (reminderDays.isEmpty) reminderDays = WeekdayList.EVERY_DAY + if (isSkipDays) reminderDays = WeekdayList(reminderDays.toArray(),listSkipDays.toArray()) populateReminder() } dialog.setSelectedDays(reminderDays) dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker") } + populateSkipDays() + binding.skipDaysPicker.setOnClickListener { + val dialog = WeekdayPickerDialog() + + dialog.setListener { days: WeekdayList -> + listSkipDays = days + if (listSkipDays.isEmpty) listSkipDays = WeekdayList.NO_DAY + isSkipDays = (listSkipDays != WeekdayList.NO_DAY) + if (reminderHour >= 0 && isSkipDays) { + reminderDays = WeekdayList(reminderDays.toArray(),listSkipDays.toArray()) + populateReminder() + } + populateSkipDays() + } + dialog.setSelectedDays(listSkipDays) + dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker") + } + binding.buttonSave.setOnClickListener { if (validate()) save() } @@ -277,6 +302,8 @@ class EditHabitActivity : AppCompatActivity() { } habit.frequency = Frequency(freqNum, freqDen) + habit.skipDays = isSkipDays + habit.skipDaysList = listSkipDays if (habitType == HabitType.NUMERICAL) { habit.targetValue = binding.targetInput.text.toString().toDouble() habit.targetType = targetType @@ -330,6 +357,14 @@ class EditHabitActivity : AppCompatActivity() { } } + private fun populateSkipDays() { + if (isSkipDays) { + binding.skipDaysPicker.text = listSkipDays.toFormattedString(this) + } else { + binding.skipDaysPicker.text = getString(R.string.skip_days_off) + } + } + @SuppressLint("StringFormatMatches") private fun populateFrequency() { binding.booleanFrequencyPicker.text = formatFrequency(freqNum, freqDen, resources) @@ -372,6 +407,8 @@ class EditHabitActivity : AppCompatActivity() { putInt("androidColor", androidColor) putInt("freqNum", freqNum) putInt("freqDen", freqDen) + putBoolean("isSkipDays", isSkipDays) + putInt("listSkipDays", listSkipDays.toInteger()) putInt("reminderHour", reminderHour) putInt("reminderMin", reminderMin) putInt("reminderDays", reminderDays.toInteger()) diff --git a/uhabits-android/src/main/res/layout/activity_edit_habit.xml b/uhabits-android/src/main/res/layout/activity_edit_habit.xml index 75dddb551..06ff19f35 100644 --- a/uhabits-android/src/main/res/layout/activity_edit_habit.xml +++ b/uhabits-android/src/main/res/layout/activity_edit_habit.xml @@ -73,6 +73,7 @@ android:paddingRight="4dp"> + + + + + + + + + + + History Clear Reminder + Skip days Save Streaks You have no active habits You\'re all done for today! Press-and-hold to check or uncheck Off + Off Create habit Edit habit Check diff --git a/uhabits-core-legacy/assets/main/migrations/009.sql b/uhabits-core-legacy/assets/main/migrations/009.sql index 5a4afd962..8a37d40fd 100644 --- a/uhabits-core-legacy/assets/main/migrations/009.sql +++ b/uhabits-core-legacy/assets/main/migrations/009.sql @@ -1,4 +1,4 @@ -create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer, highlight integer, name text, position integer, reminder_hour integer, reminder_min integer ) +create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer, skip_days integer, skip_days_list integer, highlight integer, name text, position integer, reminder_hour integer, reminder_min integer ) create table Checkmarks ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer, value integer ) create table Repetitions ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer ) create table Streak ( id integer primary key autoincrement, end integer, habit integer references habits(id), length integer, start integer ) diff --git a/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt b/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt index 961956f51..20e86fcbe 100644 --- a/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt +++ b/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt @@ -28,9 +28,9 @@ import org.isoron.platform.io.nextId class HabitRepository(var db: Database) { companion object { - const val SELECT_COLUMNS = "id, name, description, freq_num, freq_den, color, archived, position, unit, target_value, type" - const val SELECT_PLACEHOLDERS = "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?" - const val UPDATE_COLUMNS = "id=?, name=?, description=?, freq_num=?, freq_den=?, color=?, archived=?, position=?, unit=?, target_value=?, type=?" + const val SELECT_COLUMNS = "id, name, description, freq_num, freq_den, skip_days, skip_days_list, color, archived, position, unit, target_value, type" + const val SELECT_PLACEHOLDERS = "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?" + const val UPDATE_COLUMNS = "id=?, name=?, description=?, freq_num=?, freq_den=?, skip_days=?, skip_days_list=?, color=?, archived=?, position=?, unit=?, target_value=?, type=?" } private val findAllStatement = db.prepareStatement("select $SELECT_COLUMNS from habits order by position") @@ -60,7 +60,7 @@ class HabitRepository(var db: Database) { fun update(habit: Habit) { bindHabitToStatement(habit, updateStatement) - updateStatement.bindInt(11, habit.id) + updateStatement.bindInt(13, habit.id) updateStatement.step() updateStatement.reset() } @@ -70,12 +70,14 @@ class HabitRepository(var db: Database) { name = stmt.getText(1), description = stmt.getText(2), frequency = Frequency(stmt.getInt(3), stmt.getInt(4)), - color = PaletteColor(stmt.getInt(5)), - isArchived = stmt.getInt(6) != 0, - position = stmt.getInt(7), - unit = stmt.getText(8), - target = stmt.getReal(9), - type = if (stmt.getInt(10) == 0) HabitType.BOOLEAN_HABIT else HabitType.NUMERICAL_HABIT) + skipDays = (stmt.getInt(5) == 1), + skipDaysList = WeekDayList(stmt.getInt(6)), + color = PaletteColor(stmt.getInt(7)), + isArchived = stmt.getInt(8) != 0, + position = stmt.getInt(9), + unit = stmt.getText(10), + target = stmt.getReal(11), + type = if (stmt.getInt(12) == 0) HabitType.BOOLEAN_HABIT else HabitType.NUMERICAL_HABIT) } private fun bindHabitToStatement(habit: Habit, statement: PreparedStatement) { @@ -84,12 +86,14 @@ class HabitRepository(var db: Database) { statement.bindText(2, habit.description) statement.bindInt(3, habit.frequency.numerator) statement.bindInt(4, habit.frequency.denominator) - statement.bindInt(5, habit.color.index) - statement.bindInt(6, if (habit.isArchived) 1 else 0) - statement.bindInt(7, habit.position) - statement.bindText(8, habit.unit) - statement.bindReal(9, habit.target) - statement.bindInt(10, habit.type.code) + statement.bindInt(5, if (habit.skipDays) 1 else 0) + statement.bindInt(6, habit.skipDaysList.toInteger()) + statement.bindInt(7, habit.color.index) + statement.bindInt(8, if (habit.isArchived) 1 else 0) + statement.bindInt(9, habit.position) + statement.bindText(10, habit.unit) + statement.bindReal(11, habit.target) + statement.bindInt(12, habit.type.code) } fun delete(habit: Habit) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt index 5c5499f49..f04207be2 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt @@ -38,11 +38,12 @@ open class EntryList { /** * Returns the entry corresponding to the given timestamp. If no entry with such timestamp - * has been previously added, returns Entry(timestamp, UNKNOWN). + * has been previously added, returns Entry(timestamp, UNKNOWN). or Entry(timestamp, SKIP) if + * skip days are enabled and that day is to be skipped */ @Synchronized - open fun get(timestamp: Timestamp): Entry { - return entriesByTimestamp[timestamp] ?: Entry(timestamp, UNKNOWN) + open fun get(timestamp: Timestamp, skipDays: Boolean = false, skipDaysList: WeekdayList = WeekdayList.NO_DAY): Entry { + return entriesByTimestamp[timestamp] ?: if (skipDays && skipDaysList.isDayTrue(timestamp.weekday)) Entry(timestamp, SKIP) else Entry(timestamp, UNKNOWN) } /** @@ -51,12 +52,12 @@ open class EntryList { * included. */ @Synchronized - open fun getByInterval(from: Timestamp, to: Timestamp): List { + open fun getByInterval(from: Timestamp, to: Timestamp, skipDays: Boolean = false, skipDaysList: WeekdayList = WeekdayList.NO_DAY): List { val result = mutableListOf() if (from.isNewerThan(to)) return result var current = to while (current >= from) { - result.add(get(current)) + result.add(get(current, skipDays, skipDaysList)) current = current.minus(1) } return result diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt index b8673211a..64ee3287f 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt @@ -20,7 +20,7 @@ package org.isoron.uhabits.core.models data class Frequency( var numerator: Int, - var denominator: Int + var denominator: Int, ) { init { if (numerator == denominator) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index a06d01ec9..0f3a7272b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -25,6 +25,8 @@ data class Habit( var color: PaletteColor = PaletteColor(8), var description: String = "", var frequency: Frequency = Frequency.DAILY, + var skipDays: Boolean = false, + var skipDaysList: WeekdayList = WeekdayList.NO_DAY, var id: Long? = null, var isArchived: Boolean = false, var name: String = "", @@ -90,6 +92,8 @@ data class Habit( scores.recompute( frequency = frequency, isNumerical = isNumerical, + skipDays = skipDays, + skipDaysList = skipDaysList, numericalHabitType = targetType, targetValue = targetValue, computedEntries = computedEntries, @@ -108,6 +112,8 @@ data class Habit( this.color = other.color this.description = other.description this.frequency = other.frequency + this.skipDays = other.skipDays + this.skipDaysList = other.skipDaysList // this.id should not be copied this.isArchived = other.isArchived this.name = other.name @@ -128,6 +134,8 @@ data class Habit( if (color != other.color) return false if (description != other.description) return false if (frequency != other.frequency) return false + if (skipDays != other.skipDays) return false + if (skipDaysList != other.skipDaysList) return false if (id != other.id) return false if (isArchived != other.isArchived) return false if (name != other.name) return false @@ -147,6 +155,7 @@ data class Habit( var result = color.hashCode() result = 31 * result + description.hashCode() result = 31 * result + frequency.hashCode() + result = 31 * result + skipDaysList.hashCode() result = 31 * result + (id?.hashCode() ?: 0) result = 31 * result + isArchived.hashCode() result = 31 * result + name.hashCode() 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 3acc19e1c..94c093da9 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 @@ -68,6 +68,8 @@ class ScoreList { fun recompute( frequency: Frequency, isNumerical: Boolean, + skipDays: Boolean, + skipDaysList: WeekdayList, numericalHabitType: NumericalHabitType, targetValue: Double, computedEntries: EntryList, @@ -79,7 +81,7 @@ class ScoreList { var numerator = frequency.numerator var denominator = frequency.denominator val freq = frequency.toDouble() - val values = computedEntries.getByInterval(from, to).map { it.value }.toIntArray() + val values = computedEntries.getByInterval(from, to, skipDays, skipDaysList).map { it.value }.toIntArray() val isAtMost = numericalHabitType == NumericalHabitType.AT_MOST // For non-daily boolean habits, we double the numerator and the denominator to smooth diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/WeekdayList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/WeekdayList.kt index 55dfa3b9b..1430a7dc9 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/WeekdayList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/WeekdayList.kt @@ -38,6 +38,13 @@ class WeekdayList { this.weekdays = Arrays.copyOf(weekdays, 7) } + constructor(addDays: BooleanArray, removeDays: BooleanArray) { + weekdays = BooleanArray(7) + for (i in 0..6) { + weekdays[i] = addDays[i] && !removeDays[i] + } + } + val isEmpty: Boolean get() { for (d in weekdays) if (d) return false @@ -58,6 +65,10 @@ class WeekdayList { return packedList } + fun isDayTrue(dayNum: Int): Boolean { + return weekdays[dayNum] + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false @@ -73,5 +84,6 @@ class WeekdayList { companion object { val EVERY_DAY = WeekdayList(127) + val NO_DAY = WeekdayList(booleanArrayOf(false, false, false, false, false, false, false)) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt index 128accd13..e3186fad7 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt @@ -25,6 +25,7 @@ import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.EntryList import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.core.models.sqlite.records.EntryRecord class SQLiteEntryList(database: Database) : EntryList() { @@ -43,14 +44,13 @@ class SQLiteEntryList(database: Database) : EntryList() { isLoaded = true } - override fun get(timestamp: Timestamp): Entry { + override fun get(timestamp: Timestamp, skipDays: Boolean, skipDaysList: WeekdayList): Entry { loadRecords() - return super.get(timestamp) + return super.get(timestamp, skipDays, skipDaysList) } - - override fun getByInterval(from: Timestamp, to: Timestamp): List { + override fun getByInterval(from: Timestamp, to: Timestamp, skipDays: Boolean, skipDaysList: WeekdayList): List { loadRecords() - return super.getByInterval(from, to) + return super.getByInterval(from, to, skipDays, skipDaysList) } override fun add(entry: Entry) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt index dc0386799..c30c94bd2 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt @@ -49,6 +49,12 @@ class HabitRecord { @field:Column(name = "freq_den") var freqDen: Int? = null + @field:Column(name = "skip_days") + var skipDays: Int? = null + + @field:Column(name = "skip_days_list") + var skipDaysList: Int? = null + @field:Column var color: Int? = null @@ -105,6 +111,8 @@ class HabitRecord { val (numerator, denominator) = model.frequency freqNum = numerator freqDen = denominator + skipDays = if (model.skipDays) 1 else 0 + skipDaysList = model.skipDaysList.toInteger() reminderDays = 0 reminderMin = null reminderHour = null @@ -122,6 +130,8 @@ class HabitRecord { habit.description = description!! habit.question = question!! habit.frequency = Frequency(freqNum!!, freqDen!!) + habit.skipDays = (skipDays!! == 1) + habit.skipDaysList = WeekdayList(skipDaysList!!) habit.color = PaletteColor(color!!) habit.isArchived = archived != 0 habit.type = HabitType.fromInt(type!!) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index c7d861813..cbf64201d 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -311,7 +311,9 @@ class HabitCardListCache @Inject constructor( newData.scores[habit.id] = habit.scores[today].value val list: MutableList = ArrayList() val notes: MutableList = ArrayList() - for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) { + val skipDays = habit.skipDays + val skipDaysList = habit.skipDaysList + for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today, skipDays, skipDaysList)) { list.add(value) notes.add(note) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index b66b08be6..d9682ab04 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -56,7 +56,8 @@ open class ListHabitsBehavior @Inject constructor( if (habit.type == HabitType.NUMERICAL) { val oldValue = entry.value.toDouble() / 1000 screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String, x: Float, y: Float -> - val value = (newValue * 1000).roundToInt() + val value = if (habit.skipDays && habit.skipDaysList.isDayTrue(timestamp.weekday)) 3 else (newValue * 1000).roundToInt() + if (newValue != oldValue) { if ( (habit.targetType == AT_LEAST && newValue >= habit.targetValue) || diff --git a/uhabits-core/src/jvmMain/resources/migrations/09.sql b/uhabits-core/src/jvmMain/resources/migrations/09.sql index 5fb4502b4..d8dff31f8 100644 --- a/uhabits-core/src/jvmMain/resources/migrations/09.sql +++ b/uhabits-core/src/jvmMain/resources/migrations/09.sql @@ -5,6 +5,8 @@ create table Habits ( description text, freq_den integer, freq_num integer, + skip_days integer, + skip_days_list integer, highlight integer, name text, position integer, From f5da00a989b1b0b6444be8477f521c25a40bc6c7 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Mon, 20 May 2024 12:56:56 +0200 Subject: [PATCH 03/51] All scores, frequency, history, etc. corrected for the skip days features. --- .../habits/edit/EditHabitActivity.kt | 14 ++-- .../habits/show/views/SubtitleCardView.kt | 7 ++ .../main/res/layout/activity_edit_habit.xml | 1 + .../main/res/layout/show_habit_subtitle.xml | 12 ++++ .../assets/main/migrations/009.sql | 2 +- .../assets/main/migrations/024.sql | 2 + .../isoron/uhabits/core/models/EntryList.kt | 66 ++++++++++++++----- .../org/isoron/uhabits/core/models/Habit.kt | 11 ++-- .../isoron/uhabits/core/models/ScoreList.kt | 5 +- .../isoron/uhabits/core/models/SkipDays.kt | 41 ++++++++++++ .../core/models/sqlite/SQLiteEntryList.kt | 11 ++-- .../core/models/sqlite/records/HabitRecord.kt | 8 +-- .../screens/habits/list/HabitCardListCache.kt | 3 +- .../screens/habits/list/ListHabitsBehavior.kt | 4 +- .../habits/show/views/FrequencyCard.kt | 2 +- .../screens/habits/show/views/HistoryCard.kt | 4 +- .../screens/habits/show/views/SubtitleCard.kt | 3 + .../screens/habits/show/views/TargetCard.kt | 51 +++++++++----- .../src/jvmMain/resources/migrations/09.sql | 2 - .../src/jvmMain/resources/migrations/26.sql | 2 + 20 files changed, 186 insertions(+), 65 deletions(-) create mode 100644 uhabits-core-legacy/assets/main/migrations/024.sql create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/SkipDays.kt create mode 100644 uhabits-core/src/jvmMain/resources/migrations/26.sql diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt index ab2371afc..7aaaae1da 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt @@ -51,6 +51,7 @@ import org.isoron.uhabits.core.models.HabitType import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.Reminder +import org.isoron.uhabits.core.models.SkipDays import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.databinding.ActivityEditHabitBinding import org.isoron.uhabits.utils.ColorUtils @@ -106,8 +107,8 @@ class EditHabitActivity : AppCompatActivity() { color = habit.color freqNum = habit.frequency.numerator freqDen = habit.frequency.denominator - isSkipDays = habit.skipDays - listSkipDays = habit.skipDaysList + isSkipDays = habit.skipDays.isSkipDays + listSkipDays = habit.skipDays.days targetType = habit.targetType habit.reminder?.let { reminderHour = it.hour @@ -302,8 +303,7 @@ class EditHabitActivity : AppCompatActivity() { } habit.frequency = Frequency(freqNum, freqDen) - habit.skipDays = isSkipDays - habit.skipDaysList = listSkipDays + habit.skipDays = SkipDays(isSkipDays, listSkipDays) if (habitType == HabitType.NUMERICAL) { habit.targetValue = binding.targetInput.text.toString().toDouble() habit.targetType = targetType @@ -358,6 +358,12 @@ class EditHabitActivity : AppCompatActivity() { } private fun populateSkipDays() { + val preferences = (application as HabitsApplication).component.preferences + if (preferences.isSkipEnabled || isSkipDays) { + binding.skipDaysOuterBox.visibility = View.VISIBLE + } else { + binding.skipDaysOuterBox.visibility = View.GONE + } if (isSkipDays) { binding.skipDaysPicker.text = listSkipDays.toFormattedString(this) } else { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCardView.kt index c29894c18..5e49755be 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCardView.kt @@ -33,6 +33,7 @@ import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding import org.isoron.uhabits.utils.InterfaceUtils import org.isoron.uhabits.utils.formatTime +import org.isoron.uhabits.utils.toFormattedString class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { @@ -78,6 +79,12 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con if (state.question.isEmpty()) { binding.questionLabel.visibility = View.GONE } + if (state.skipDays.isSkipDays) { + binding.skipLabel.visibility = View.VISIBLE + binding.skipLabel.text = context.getString(R.string.skip_day) + " " + state.skipDays.days.toFormattedString(context) + } else { + binding.skipLabel.visibility = View.GONE + } postInvalidate() } diff --git a/uhabits-android/src/main/res/layout/activity_edit_habit.xml b/uhabits-android/src/main/res/layout/activity_edit_habit.xml index 06ff19f35..b001f8105 100644 --- a/uhabits-android/src/main/res/layout/activity_edit_habit.xml +++ b/uhabits-android/src/main/res/layout/activity_edit_habit.xml @@ -251,6 +251,7 @@ diff --git a/uhabits-android/src/main/res/layout/show_habit_subtitle.xml b/uhabits-android/src/main/res/layout/show_habit_subtitle.xml index 7361b4565..fad407f4a 100644 --- a/uhabits-android/src/main/res/layout/show_habit_subtitle.xml +++ b/uhabits-android/src/main/res/layout/show_habit_subtitle.xml @@ -76,6 +76,17 @@ android:text="@string/every_day" android:textColor="?attr/contrast60" android:layout_marginStart="4dp" + android:layout_marginEnd="8dp" + android:textSize="@dimen/smallTextSize" /> + + @@ -95,6 +106,7 @@ android:textColor="?attr/contrast60" android:text="8:00 AM" android:layout_marginStart="4dp" + android:layout_marginEnd="16dp" android:textSize="@dimen/smallTextSize" /> diff --git a/uhabits-core-legacy/assets/main/migrations/009.sql b/uhabits-core-legacy/assets/main/migrations/009.sql index 8a37d40fd..1fae1a722 100644 --- a/uhabits-core-legacy/assets/main/migrations/009.sql +++ b/uhabits-core-legacy/assets/main/migrations/009.sql @@ -1,4 +1,4 @@ -create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer, skip_days integer, skip_days_list integer, highlight integer, name text, position integer, reminder_hour integer, reminder_min integer ) +create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer highlight integer, name text, position integer, reminder_hour integer, reminder_min integer ) create table Checkmarks ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer, value integer ) create table Repetitions ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer ) create table Streak ( id integer primary key autoincrement, end integer, habit integer references habits(id), length integer, start integer ) diff --git a/uhabits-core-legacy/assets/main/migrations/024.sql b/uhabits-core-legacy/assets/main/migrations/024.sql new file mode 100644 index 000000000..3899636ea --- /dev/null +++ b/uhabits-core-legacy/assets/main/migrations/024.sql @@ -0,0 +1,2 @@ +alter table Habits add column skip_days integer not null default 0 +alter table Habits add column skip_days_list integer not null default 0 \ No newline at end of file diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt index f04207be2..fa5fe7e9f 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt @@ -42,8 +42,8 @@ open class EntryList { * skip days are enabled and that day is to be skipped */ @Synchronized - open fun get(timestamp: Timestamp, skipDays: Boolean = false, skipDaysList: WeekdayList = WeekdayList.NO_DAY): Entry { - return entriesByTimestamp[timestamp] ?: if (skipDays && skipDaysList.isDayTrue(timestamp.weekday)) Entry(timestamp, SKIP) else Entry(timestamp, UNKNOWN) + open fun get(timestamp: Timestamp, skipDays: SkipDays = SkipDays.NONE): Entry { + return if (skipDays.isDaySkipped(timestamp)) Entry(timestamp, SKIP) else entriesByTimestamp[timestamp] ?: Entry(timestamp, UNKNOWN) } /** @@ -52,12 +52,12 @@ open class EntryList { * included. */ @Synchronized - open fun getByInterval(from: Timestamp, to: Timestamp, skipDays: Boolean = false, skipDaysList: WeekdayList = WeekdayList.NO_DAY): List { + open fun getByInterval(from: Timestamp, to: Timestamp, skipDays: SkipDays = SkipDays.NONE): List { val result = mutableListOf() if (from.isNewerThan(to)) return result var current = to while (current >= from) { - result.add(get(current, skipDays, skipDaysList)) + result.add(get(current, skipDays)) current = current.minus(1) } return result @@ -91,16 +91,18 @@ open class EntryList { open fun recomputeFrom( originalEntries: EntryList, frequency: Frequency, - isNumerical: Boolean + isNumerical: Boolean, + skipDays: SkipDays ) { clear() val original = originalEntries.getKnown() if (isNumerical) { - original.forEach { add(it) } + val computed = addEntriesWithSkipDays(original, skipDays) + computed.forEach { add(it) } } else { val intervals = buildIntervals(frequency, original) snapIntervalsTogether(intervals) - val computed = buildEntriesFromInterval(original, intervals) + val computed = buildEntriesFromInterval(original, intervals, skipDays) computed.filter { it.value != UNKNOWN || it.notes.isNotEmpty() }.forEach { add(it) } } } @@ -129,10 +131,10 @@ open class EntryList { fun computeWeekdayFrequency(isNumerical: Boolean): HashMap> { val entries = getKnown() val map = hashMapOf>() - for ((originalTimestamp, value) in entries) { - val weekday = originalTimestamp.weekday + for ((computedTimestamp, value) in entries) { + val weekday = computedTimestamp.weekday val truncatedTimestamp = Timestamp( - originalTimestamp.toCalendar().apply { + computedTimestamp.toCalendar().apply { set(Calendar.DAY_OF_MONTH, 1) }.timeInMillis ) @@ -143,7 +145,7 @@ open class EntryList { map[truncatedTimestamp] = list } - if (isNumerical) { + if (isNumerical && value != SKIP) { list[weekday] += value } else if (value == YES_MANUAL) { list[weekday] += 1 @@ -168,7 +170,8 @@ open class EntryList { */ fun buildEntriesFromInterval( original: List, - intervals: List + intervals: List, + skipDays: SkipDays ): List { val result = arrayListOf() if (original.isEmpty()) return result @@ -197,15 +200,17 @@ open class EntryList { current = interval.end while (current >= interval.begin) { val offset = current.daysUntil(to) - result[offset] = Entry(current, YES_AUTO) + result[offset] = if (skipDays.isDaySkipped(current)) Entry(current, SKIP) else Entry(current, YES_AUTO) current = current.minus(1) } } - // Copy original entries + // Copy original entries except for the skipped days original.forEach { entry -> val offset = entry.timestamp.daysUntil(to) - val value = if ( + val value = if (skipDays.isDaySkipped(entry)) { + SKIP + } else if ( result[offset].value == UNKNOWN || entry.value == SKIP || entry.value == YES_MANUAL @@ -272,6 +277,29 @@ open class EntryList { } return intervals } + + fun addEntriesWithSkipDays(original: List, skipDays: SkipDays): List { + if (original.isEmpty()) return original + val earliest = original.last().timestamp + val today = DateUtils.getTodayWithOffset() + val computed = mutableListOf() + var current = today + var offset = 0 + while (current >= earliest) { + if (current == original[offset].timestamp) { + if (!skipDays.isDaySkipped(current)) { + computed.add(original[offset]) + } else { + computed.add(Entry(current, SKIP)) + } + offset++ + } else if (skipDays.isDaySkipped(current)) { + computed.add(Entry(current, SKIP)) + } + current = current.minus(1) + } + return computed + } } } @@ -324,10 +352,12 @@ fun List.groupedSum( */ fun List.countSkippedDays( truncateField: DateUtils.TruncateField, - firstWeekday: Int = Calendar.SATURDAY + firstWeekday: Int = Calendar.SATURDAY, + skipDays: SkipDays ): List { + val thisIntervalStart = DateUtils.getTodayWithOffset().truncate(truncateField, firstWeekday) return this.map { (timestamp, value) -> - if (value == SKIP) { + if (value == SKIP || skipDays.isDaySkipped(timestamp)) { Entry(timestamp, 1) } else { Entry(timestamp, 0) @@ -341,5 +371,5 @@ fun List.countSkippedDays( Entry(timestamp, entries.sumOf { it.value }) }.sortedBy { (timestamp, _) -> -timestamp.unixTime - } + }.filter { it.timestamp == thisIntervalStart } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index 0f3a7272b..5a87695b7 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -25,8 +25,7 @@ data class Habit( var color: PaletteColor = PaletteColor(8), var description: String = "", var frequency: Frequency = Frequency.DAILY, - var skipDays: Boolean = false, - var skipDaysList: WeekdayList = WeekdayList.NO_DAY, + var skipDays: SkipDays = SkipDays.NONE, var id: Long? = null, var isArchived: Boolean = false, var name: String = "", @@ -80,7 +79,8 @@ data class Habit( computedEntries.recomputeFrom( originalEntries = originalEntries, frequency = frequency, - isNumerical = isNumerical + isNumerical = isNumerical, + skipDays = skipDays ) val today = DateUtils.getTodayWithOffset() @@ -93,7 +93,6 @@ data class Habit( frequency = frequency, isNumerical = isNumerical, skipDays = skipDays, - skipDaysList = skipDaysList, numericalHabitType = targetType, targetValue = targetValue, computedEntries = computedEntries, @@ -113,7 +112,6 @@ data class Habit( this.description = other.description this.frequency = other.frequency this.skipDays = other.skipDays - this.skipDaysList = other.skipDaysList // this.id should not be copied this.isArchived = other.isArchived this.name = other.name @@ -135,7 +133,6 @@ data class Habit( if (description != other.description) return false if (frequency != other.frequency) return false if (skipDays != other.skipDays) return false - if (skipDaysList != other.skipDaysList) return false if (id != other.id) return false if (isArchived != other.isArchived) return false if (name != other.name) return false @@ -155,7 +152,7 @@ data class Habit( var result = color.hashCode() result = 31 * result + description.hashCode() result = 31 * result + frequency.hashCode() - result = 31 * result + skipDaysList.hashCode() + result = 31 * result + skipDays.hashCode() result = 31 * result + (id?.hashCode() ?: 0) result = 31 * result + isArchived.hashCode() result = 31 * result + name.hashCode() 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 94c093da9..84945ad97 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 @@ -68,8 +68,7 @@ class ScoreList { fun recompute( frequency: Frequency, isNumerical: Boolean, - skipDays: Boolean, - skipDaysList: WeekdayList, + skipDays: SkipDays, numericalHabitType: NumericalHabitType, targetValue: Double, computedEntries: EntryList, @@ -81,7 +80,7 @@ class ScoreList { var numerator = frequency.numerator var denominator = frequency.denominator val freq = frequency.toDouble() - val values = computedEntries.getByInterval(from, to, skipDays, skipDaysList).map { it.value }.toIntArray() + val values = computedEntries.getByInterval(from, to, skipDays).map { it.value }.toIntArray() val isAtMost = numericalHabitType == NumericalHabitType.AT_MOST // For non-daily boolean habits, we double the numerator and the denominator to smooth diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/SkipDays.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/SkipDays.kt new file mode 100644 index 000000000..6815c700e --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/SkipDays.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.models + +data class SkipDays( + val isSkipDays: Boolean, + val days: WeekdayList +) { + fun isDaySkipped(day: Int): Boolean { + return isSkipDays && days.isDayTrue(day) + } + + fun isDaySkipped(day: Timestamp): Boolean { + return isSkipDays && days.isDayTrue(day.weekday) + } + + fun isDaySkipped(entry: Entry): Boolean { + return isSkipDays && days.isDayTrue(entry.timestamp.weekday) + } + + companion object { + @JvmField + val NONE = SkipDays(false, WeekdayList(0)) + } +} \ No newline at end of file diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt index e3186fad7..895ff8f0d 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt @@ -24,6 +24,7 @@ import org.isoron.uhabits.core.database.Repository import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.EntryList import org.isoron.uhabits.core.models.Frequency +import org.isoron.uhabits.core.models.SkipDays import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.core.models.sqlite.records.EntryRecord @@ -44,13 +45,13 @@ class SQLiteEntryList(database: Database) : EntryList() { isLoaded = true } - override fun get(timestamp: Timestamp, skipDays: Boolean, skipDaysList: WeekdayList): Entry { + override fun get(timestamp: Timestamp, skipDays: SkipDays): Entry { loadRecords() - return super.get(timestamp, skipDays, skipDaysList) + return super.get(timestamp, skipDays) } - override fun getByInterval(from: Timestamp, to: Timestamp, skipDays: Boolean, skipDaysList: WeekdayList): List { + override fun getByInterval(from: Timestamp, to: Timestamp, skipDays: SkipDays): List { loadRecords() - return super.getByInterval(from, to, skipDays, skipDaysList) + return super.getByInterval(from, to, skipDays) } override fun add(entry: Entry) { @@ -78,7 +79,7 @@ class SQLiteEntryList(database: Database) : EntryList() { return super.getKnown() } - override fun recomputeFrom(originalEntries: EntryList, frequency: Frequency, isNumerical: Boolean) { + override fun recomputeFrom(originalEntries: EntryList, frequency: Frequency, isNumerical: Boolean, skipDays: SkipDays) { throw UnsupportedOperationException() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt index c30c94bd2..e4324a214 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt @@ -26,6 +26,7 @@ import org.isoron.uhabits.core.models.HabitType import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.Reminder +import org.isoron.uhabits.core.models.SkipDays import org.isoron.uhabits.core.models.WeekdayList import java.util.Objects.requireNonNull @@ -111,8 +112,8 @@ class HabitRecord { val (numerator, denominator) = model.frequency freqNum = numerator freqDen = denominator - skipDays = if (model.skipDays) 1 else 0 - skipDaysList = model.skipDaysList.toInteger() + skipDays = if (model.skipDays.isSkipDays) 1 else 0 + skipDaysList = model.skipDays.days.toInteger() reminderDays = 0 reminderMin = null reminderHour = null @@ -130,8 +131,7 @@ class HabitRecord { habit.description = description!! habit.question = question!! habit.frequency = Frequency(freqNum!!, freqDen!!) - habit.skipDays = (skipDays!! == 1) - habit.skipDaysList = WeekdayList(skipDaysList!!) + habit.skipDays = SkipDays(skipDays!! == 1, WeekdayList(skipDaysList!!)) habit.color = PaletteColor(color!!) habit.isArchived = archived != 0 habit.type = HabitType.fromInt(type!!) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index cbf64201d..6615dcf60 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -312,8 +312,7 @@ class HabitCardListCache @Inject constructor( val list: MutableList = ArrayList() val notes: MutableList = ArrayList() val skipDays = habit.skipDays - val skipDaysList = habit.skipDaysList - for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today, skipDays, skipDaysList)) { + for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today, skipDays)) { list.add(value) notes.add(note) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index d9682ab04..703a8cf0a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -20,6 +20,7 @@ package org.isoron.uhabits.core.ui.screens.habits.list import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateRepetitionCommand +import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitList @@ -53,10 +54,11 @@ open class ListHabitsBehavior @Inject constructor( fun onEdit(habit: Habit, timestamp: Timestamp?) { val entry = habit.computedEntries.get(timestamp!!) + if (habit.skipDays.isDaySkipped(timestamp)) return if (habit.type == HabitType.NUMERICAL) { val oldValue = entry.value.toDouble() / 1000 screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String, x: Float, y: Float -> - val value = if (habit.skipDays && habit.skipDaysList.isDayTrue(timestamp.weekday)) 3 else (newValue * 1000).roundToInt() + val value = if (habit.skipDays.isDaySkipped(timestamp)) Entry.SKIP else (newValue * 1000).roundToInt() if (newValue != oldValue) { if ( diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt index 0246daf7d..8a233109e 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt @@ -42,7 +42,7 @@ class FrequencyCardPresenter { ) = FrequencyCardState( color = habit.color, isNumerical = habit.isNumerical, - frequency = habit.originalEntries.computeWeekdayFrequency( + frequency = habit.computedEntries.computeWeekdayFrequency( isNumerical = habit.isNumerical ), firstWeekday = firstWeekday, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt index 0a28c801f..73f7be416 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt @@ -67,6 +67,7 @@ class HistoryCardPresenter( override fun onDateLongPress(date: LocalDate) { val timestamp = Timestamp.fromLocalDate(date) screen.showFeedback() + if (habit.skipDays.isDaySkipped(timestamp)) return if (habit.isNumerical) { showNumberPopup(timestamp) } else { @@ -81,6 +82,7 @@ class HistoryCardPresenter( override fun onDateShortPress(date: LocalDate) { val timestamp = Timestamp.fromLocalDate(date) screen.showFeedback() + if (habit.skipDays.isDaySkipped(timestamp)) return if (habit.isNumerical) { showNumberPopup(timestamp) } else { @@ -161,7 +163,7 @@ class HistoryCardPresenter( ): HistoryCardState { val today = DateUtils.getTodayWithOffset() val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today - val entries = habit.computedEntries.getByInterval(oldest, today) + val entries = habit.computedEntries.getByInterval(oldest, today, habit.skipDays) val series = if (habit.isNumerical) { entries.map { when { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt index fb839933c..01784e2f2 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt @@ -24,12 +24,14 @@ import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.Reminder +import org.isoron.uhabits.core.models.SkipDays import org.isoron.uhabits.core.ui.views.Theme data class SubtitleCardState( val color: PaletteColor, val frequency: Frequency, val isNumerical: Boolean, + val skipDays: SkipDays, val question: String, val reminder: Reminder?, val targetValue: Double = 0.0, @@ -47,6 +49,7 @@ class SubtitleCardPresenter { color = habit.color, frequency = habit.frequency, isNumerical = habit.isNumerical, + skipDays = habit.skipDays, question = habit.question, reminder = habit.reminder, targetValue = habit.targetValue, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt index 49c95011d..3ff074424 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt @@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.PaletteColor +import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.countSkippedDays import org.isoron.uhabits.core.models.groupedSum import org.isoron.uhabits.core.ui.views.Theme @@ -45,54 +46,61 @@ class TargetCardPresenter { theme: Theme ): TargetCardState { val today = DateUtils.getTodayWithOffset() + val (yearBegin, yearEnd) = getYearRange(firstWeekday) val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today - val entries = habit.computedEntries.getByInterval(oldest, today) + val entriesForSkip = habit.computedEntries.getByInterval(yearBegin, yearEnd, habit.skipDays) + val entriesForSum = habit.computedEntries.getByInterval(oldest, today) - val valueToday = entries.groupedSum( + val valueToday = entriesForSum.groupedSum( truncateField = DateUtils.TruncateField.DAY, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 - val skippedDayToday = entries.countSkippedDays( - truncateField = DateUtils.TruncateField.DAY + val skippedDayToday = entriesForSkip.countSkippedDays( + truncateField = DateUtils.TruncateField.DAY, + skipDays = habit.skipDays, ).firstOrNull()?.value ?: 0 - val valueThisWeek = entries.groupedSum( + val valueThisWeek = entriesForSum.groupedSum( truncateField = DateUtils.TruncateField.WEEK_NUMBER, firstWeekday = firstWeekday, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 - val skippedDaysThisWeek = entries.countSkippedDays( + val skippedDaysThisWeek = entriesForSkip.countSkippedDays( truncateField = DateUtils.TruncateField.WEEK_NUMBER, - firstWeekday = firstWeekday + firstWeekday = firstWeekday, + skipDays = habit.skipDays, ).firstOrNull()?.value ?: 0 - val valueThisMonth = entries.groupedSum( + val valueThisMonth = entriesForSum.groupedSum( truncateField = DateUtils.TruncateField.MONTH, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 - val skippedDaysThisMonth = entries.countSkippedDays( - truncateField = DateUtils.TruncateField.MONTH + val skippedDaysThisMonth = entriesForSkip.countSkippedDays( + truncateField = DateUtils.TruncateField.MONTH, + skipDays = habit.skipDays, ).firstOrNull()?.value ?: 0 - val valueThisQuarter = entries.groupedSum( + val valueThisQuarter = entriesForSum.groupedSum( truncateField = DateUtils.TruncateField.QUARTER, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 - val skippedDaysThisQuarter = entries.countSkippedDays( - truncateField = DateUtils.TruncateField.QUARTER + val skippedDaysThisQuarter = entriesForSkip.countSkippedDays( + truncateField = DateUtils.TruncateField.QUARTER, + skipDays = habit.skipDays, ).firstOrNull()?.value ?: 0 - val valueThisYear = entries.groupedSum( + val valueThisYear = entriesForSum.groupedSum( truncateField = DateUtils.TruncateField.YEAR, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 - val skippedDaysThisYear = entries.countSkippedDays( - truncateField = DateUtils.TruncateField.YEAR + val skippedDaysThisYear = entriesForSkip.countSkippedDays( + truncateField = DateUtils.TruncateField.YEAR, + skipDays = habit.skipDays, ).firstOrNull()?.value ?: 0 val cal = DateUtils.getStartOfTodayCalendarWithOffset() @@ -165,5 +173,16 @@ class TargetCardPresenter { theme = theme ) } + + private fun getYearRange(firstWeekday: Int): Pair { + val today = DateUtils.getTodayWithOffset() + val yearBegin = today.truncate(DateUtils.TruncateField.YEAR, firstWeekday) + val cali = yearBegin.toCalendar() + cali.add(Calendar.YEAR, 1) + var newest = Timestamp(cali) + val thisWeek = today.truncate(DateUtils.TruncateField.WEEK_NUMBER, firstWeekday) + if (thisWeek.daysUntil(newest) < 7) newest = thisWeek.plus(7) + return Pair(yearBegin, newest) + } } } diff --git a/uhabits-core/src/jvmMain/resources/migrations/09.sql b/uhabits-core/src/jvmMain/resources/migrations/09.sql index d8dff31f8..5fb4502b4 100644 --- a/uhabits-core/src/jvmMain/resources/migrations/09.sql +++ b/uhabits-core/src/jvmMain/resources/migrations/09.sql @@ -5,8 +5,6 @@ create table Habits ( description text, freq_den integer, freq_num integer, - skip_days integer, - skip_days_list integer, highlight integer, name text, position integer, diff --git a/uhabits-core/src/jvmMain/resources/migrations/26.sql b/uhabits-core/src/jvmMain/resources/migrations/26.sql new file mode 100644 index 000000000..e8dc1d4a3 --- /dev/null +++ b/uhabits-core/src/jvmMain/resources/migrations/26.sql @@ -0,0 +1,2 @@ +alter table Habits add column skip_days integer not null default 0; +alter table Habits add column skip_days_list integer not null default 0; \ No newline at end of file From c76446172630883f1c8f768768d02221c45c68ad Mon Sep 17 00:00:00 2001 From: Dharanish Date: Mon, 20 May 2024 15:52:52 +0200 Subject: [PATCH 04/51] Change db version --- .../main/res/layout/show_habit_subtitle.xml | 29 ++++++++++++------- .../java/org/isoron/uhabits/core/Constants.kt | 2 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/uhabits-android/src/main/res/layout/show_habit_subtitle.xml b/uhabits-android/src/main/res/layout/show_habit_subtitle.xml index fad407f4a..7d3a67b24 100644 --- a/uhabits-android/src/main/res/layout/show_habit_subtitle.xml +++ b/uhabits-android/src/main/res/layout/show_habit_subtitle.xml @@ -80,34 +80,43 @@ android:textSize="@dimen/smallTextSize" /> + + + + + \ No newline at end of file diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/Constants.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/Constants.kt index be6c9634b..328b8cd40 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/Constants.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/Constants.kt @@ -20,4 +20,4 @@ package org.isoron.uhabits.core const val DATABASE_FILENAME = "uhabits.db" -const val DATABASE_VERSION = 25 +const val DATABASE_VERSION = 26 From 053bfe116af84fac4d8acb8f66c922c388b3230e Mon Sep 17 00:00:00 2001 From: Dharanish Date: Mon, 20 May 2024 17:22:41 +0200 Subject: [PATCH 05/51] Fix linting --- .../activities/habits/edit/EditHabitActivity.kt | 4 ++-- .../java/org/isoron/uhabits/core/models/EntryList.kt | 2 +- .../java/org/isoron/uhabits/core/models/Frequency.kt | 2 +- .../java/org/isoron/uhabits/core/models/Habit.kt | 2 +- .../java/org/isoron/uhabits/core/models/SkipDays.kt | 2 +- .../uhabits/core/models/sqlite/SQLiteEntryList.kt | 1 - .../core/ui/screens/habits/show/views/TargetCard.kt | 10 +++++----- 7 files changed, 11 insertions(+), 12 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt index 7aaaae1da..482514e2a 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt @@ -248,7 +248,7 @@ class EditHabitActivity : AppCompatActivity() { dialog.setListener { days: WeekdayList -> reminderDays = days if (reminderDays.isEmpty) reminderDays = WeekdayList.EVERY_DAY - if (isSkipDays) reminderDays = WeekdayList(reminderDays.toArray(),listSkipDays.toArray()) + if (isSkipDays) reminderDays = WeekdayList(reminderDays.toArray(), listSkipDays.toArray()) populateReminder() } dialog.setSelectedDays(reminderDays) @@ -264,7 +264,7 @@ class EditHabitActivity : AppCompatActivity() { if (listSkipDays.isEmpty) listSkipDays = WeekdayList.NO_DAY isSkipDays = (listSkipDays != WeekdayList.NO_DAY) if (reminderHour >= 0 && isSkipDays) { - reminderDays = WeekdayList(reminderDays.toArray(),listSkipDays.toArray()) + reminderDays = WeekdayList(reminderDays.toArray(), listSkipDays.toArray()) populateReminder() } populateSkipDays() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt index fa5fe7e9f..12ba3dd2a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt @@ -355,7 +355,7 @@ fun List.countSkippedDays( firstWeekday: Int = Calendar.SATURDAY, skipDays: SkipDays ): List { - val thisIntervalStart = DateUtils.getTodayWithOffset().truncate(truncateField, firstWeekday) + val thisIntervalStart = DateUtils.getTodayWithOffset().truncate(truncateField, firstWeekday) return this.map { (timestamp, value) -> if (value == SKIP || skipDays.isDaySkipped(timestamp)) { Entry(timestamp, 1) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt index 64ee3287f..b8673211a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt @@ -20,7 +20,7 @@ package org.isoron.uhabits.core.models data class Frequency( var numerator: Int, - var denominator: Int, + var denominator: Int ) { init { if (numerator == denominator) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index 5a87695b7..132250754 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -25,7 +25,7 @@ data class Habit( var color: PaletteColor = PaletteColor(8), var description: String = "", var frequency: Frequency = Frequency.DAILY, - var skipDays: SkipDays = SkipDays.NONE, + var skipDays: SkipDays = SkipDays.NONE, var id: Long? = null, var isArchived: Boolean = false, var name: String = "", diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/SkipDays.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/SkipDays.kt index 6815c700e..4d386fe59 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/SkipDays.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/SkipDays.kt @@ -38,4 +38,4 @@ data class SkipDays( @JvmField val NONE = SkipDays(false, WeekdayList(0)) } -} \ No newline at end of file +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt index 895ff8f0d..824234ad2 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt @@ -26,7 +26,6 @@ import org.isoron.uhabits.core.models.EntryList import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.SkipDays import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.core.models.sqlite.records.EntryRecord class SQLiteEntryList(database: Database) : EntryList() { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt index 3ff074424..43a240dc6 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt @@ -58,7 +58,7 @@ class TargetCardPresenter { val skippedDayToday = entriesForSkip.countSkippedDays( truncateField = DateUtils.TruncateField.DAY, - skipDays = habit.skipDays, + skipDays = habit.skipDays ).firstOrNull()?.value ?: 0 val valueThisWeek = entriesForSum.groupedSum( @@ -70,7 +70,7 @@ class TargetCardPresenter { val skippedDaysThisWeek = entriesForSkip.countSkippedDays( truncateField = DateUtils.TruncateField.WEEK_NUMBER, firstWeekday = firstWeekday, - skipDays = habit.skipDays, + skipDays = habit.skipDays ).firstOrNull()?.value ?: 0 val valueThisMonth = entriesForSum.groupedSum( @@ -80,7 +80,7 @@ class TargetCardPresenter { val skippedDaysThisMonth = entriesForSkip.countSkippedDays( truncateField = DateUtils.TruncateField.MONTH, - skipDays = habit.skipDays, + skipDays = habit.skipDays ).firstOrNull()?.value ?: 0 val valueThisQuarter = entriesForSum.groupedSum( @@ -90,7 +90,7 @@ class TargetCardPresenter { val skippedDaysThisQuarter = entriesForSkip.countSkippedDays( truncateField = DateUtils.TruncateField.QUARTER, - skipDays = habit.skipDays, + skipDays = habit.skipDays ).firstOrNull()?.value ?: 0 val valueThisYear = entriesForSum.groupedSum( @@ -100,7 +100,7 @@ class TargetCardPresenter { val skippedDaysThisYear = entriesForSkip.countSkippedDays( truncateField = DateUtils.TruncateField.YEAR, - skipDays = habit.skipDays, + skipDays = habit.skipDays ).firstOrNull()?.value ?: 0 val cal = DateUtils.getStartOfTodayCalendarWithOffset() From 340bde9f69f8a0c32790a13a3cc87f46383830a2 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Thu, 23 May 2024 22:29:57 +0200 Subject: [PATCH 06/51] Create HabitGroup and HabitGroup list classes --- .../org/isoron/uhabits/core/models/Habit.kt | 4 + .../isoron/uhabits/core/models/HabitGroup.kt | 123 +++++++++++ .../uhabits/core/models/HabitGroupList.kt | 200 ++++++++++++++++++ .../isoron/uhabits/core/models/ScoreList.kt | 19 +- .../org/isoron/uhabits/core/models/Streak.kt | 4 + .../isoron/uhabits/core/models/StreakList.kt | 31 +++ 6 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroup.kt create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroupList.kt diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index 132250754..07be68a00 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -107,6 +107,10 @@ data class Habit( ) } + fun firstEntryDate(): Timestamp { + return computedEntries.getKnown().lastOrNull()?.timestamp ?: DateUtils.getTodayWithOffset() + } + fun copyFrom(other: Habit) { this.color = other.color this.description = other.description 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 new file mode 100644 index 000000000..6d8773732 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroup.kt @@ -0,0 +1,123 @@ +package org.isoron.uhabits.core.models + +import org.isoron.uhabits.core.utils.DateUtils +import java.util.UUID + +data class HabitGroup( + var color: PaletteColor = PaletteColor(8), + var description: String = "", + var id: Long? = null, + var isArchived: Boolean = false, + var name: String = "", + var position: Int = 0, + var question: String = "", + var reminder: Reminder? = null, + var unit: String = "", + var uuid: String? = null, + var habitList: HabitList, + var habitGroupList: HabitGroupList, + val scores: ScoreList, + val streaks: StreakList +) { + init { + if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "") + } + + var observable = ModelObservable() + + val uriString: String + get() = "content://org.isoron.uhabits/habit/$id" + + fun hasReminder(): Boolean = reminder != null + + fun isCompletedToday(): Boolean { + return habitList.all { it.isCompletedToday() } && habitGroupList.all { it.isCompletedToday() } + } + + fun isEnteredToday(): Boolean { + return habitList.all { it.isEnteredToday() } && habitGroupList.all { it.isEnteredToday() } + } + + fun firstEntryDate(): Timestamp { + val today = DateUtils.getTodayWithOffset() + var earliest = today + for (h in habitList) { + 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) + var from = firstEntryDate() + if (from.isNewerThan(to)) from = to + + scores.combineFrom( + habitList = habitList, + habitGroupList = habitGroupList, + from = from, + to = to + ) + + streaks.combineFrom( + habitList = habitList, + habitGroupList = habitGroupList, + from = from, + to = to + ) + } + + fun copyFrom(other: Habit) { + this.color = other.color + this.description = other.description + // this.id should not be copied + this.isArchived = other.isArchived + this.name = other.name + this.position = other.position + this.question = other.question + this.reminder = other.reminder + this.unit = other.unit + this.uuid = other.uuid + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Habit) return false + + if (color != other.color) return false + if (description != other.description) return false + if (id != other.id) return false + if (isArchived != other.isArchived) return false + if (name != other.name) return false + if (position != other.position) return false + if (question != other.question) return false + if (reminder != other.reminder) return false + if (unit != other.unit) return false + if (uuid != other.uuid) return false + + return true + } + + override fun hashCode(): Int { + var result = color.hashCode() + result = 31 * result + description.hashCode() + result = 31 * result + (id?.hashCode() ?: 0) + result = 31 * result + isArchived.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + position + result = 31 * result + question.hashCode() + result = 31 * result + (reminder?.hashCode() ?: 0) + result = 31 * result + unit.hashCode() + result = 31 * result + (uuid?.hashCode() ?: 0) + return result + } +} 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 new file mode 100644 index 000000000..583e5c82f --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroupList.kt @@ -0,0 +1,200 @@ +package org.isoron.uhabits.core.models + +import com.opencsv.CSVWriter +import java.io.IOException +import java.io.Writer +import java.util.LinkedList +import javax.annotation.concurrent.ThreadSafe + +/** + * An ordered collection of [HabitGroup]s. + */ +@ThreadSafe +abstract class HabitGroupList : Iterable { + val observable: ModelObservable + + @JvmField + protected val filter: HabitMatcher + + /** + * Creates a new HabitList. + * + * Depending on the implementation, this list can either be empty or be + * populated by some pre-existing habits, for example, from a certain + * database. + */ + constructor() { + observable = ModelObservable() + filter = HabitMatcher(isArchivedAllowed = true) + } + + protected constructor(filter: HabitMatcher) { + observable = ModelObservable() + this.filter = filter + } + + /** + * Inserts a new habit in the list. + * + * If the id of the habit is null, the list will assign it a new id, which + * is guaranteed to be unique in the scope of the list. If id is not null, + * the caller should make sure that the list does not already contain + * another habit with same id, otherwise a RuntimeException will be thrown. + * + * @param habitGroup the habit to be inserted + * @throws IllegalArgumentException if the habit is already on the list. + */ + @Throws(IllegalArgumentException::class) + abstract fun add(habitGroup: HabitGroup) + + /** + * Returns the habit with specified id. + * + * @param id the id of the habit + * @return the habit, or null if none exist + */ + abstract fun getById(id: Long): HabitGroup? + + /** + * Returns the habit with specified UUID. + * + * @param uuid the UUID of the habit + * @return the habit, or null if none exist + */ + abstract fun getByUUID(uuid: String?): HabitGroup? + + /** + * Returns the habit that occupies a certain position. + * + * @param position the position of the desired habit + * @return the habit at that position + * @throws IndexOutOfBoundsException when the position is invalid + */ + abstract fun getByPosition(position: Int): HabitGroup + + /** + * Returns the list of habits that match a given condition. + * + * @param matcher the matcher that checks the condition + * @return the list of matching habits + */ + abstract fun getFiltered(matcher: HabitMatcher?): HabitGroupList + abstract var primaryOrder: Order + abstract var secondaryOrder: Order + + /** + * Returns the index of the given habit in the list, or -1 if the list does + * not contain the habit. + * + * @param h the habit + * @return the index of the habit, or -1 if not in the list + */ + abstract fun indexOf(h: HabitGroup): Int + val isEmpty: Boolean + get() = size() == 0 + + /** + * Removes the given habit from the list. + * + * If the given habit is not in the list, does nothing. + * + * @param h the habit to be removed. + */ + abstract fun remove(h: HabitGroup) + + /** + * Removes all the habits from the list. + */ + open fun removeAll() { + val copy: MutableList = LinkedList() + for (h in this) copy.add(h) + for (h in copy) remove(h) + observable.notifyListeners() + } + + /** + * Changes the position of a habit in the list. + * + * @param from the habit that should be moved + * @param to the habit that currently occupies the desired position + */ + abstract fun reorder(from: HabitGroup, to: HabitGroup) + open fun repair() {} + + /** + * Returns the number of habits in this list. + * + * @return number of habits + */ + abstract fun size(): Int + + /** + * Notifies the list that a certain list of habits has been modified. + * + * Depending on the implementation, this operation might trigger a write to + * disk, or do nothing at all. To make sure that the habits get persisted, + * this operation must be called. + * + * @param habitGroups the list of habits that have been modified. + */ + abstract fun update(habitGroups: List) + + /** + * Notifies the list that a certain habit has been modified. + * + * See [.update] for more details. + * + * @param habitGroup the habit that has been modified. + */ + fun update(habitGroup: HabitGroup) { + update(listOf(habitGroup)) + } + + /** + * Writes the list of habits to the given writer, in CSV format. There is + * one line for each habit, containing the fields name, description, + * frequency numerator, frequency denominator and color. The color is + * written in HTML format (#000000). + * + * @param out the writer that will receive the result + * @throws IOException if write operations fail + */ + @Throws(IOException::class) + fun writeCSV(out: Writer) { + val header = arrayOf( + "Position", + "Name", + "Question", + "Description", + "NumRepetitions", + "Interval", + "Color" + ) + val csv = CSVWriter(out) + csv.writeNext(header, false) + for (habit in this) { + val cols = arrayOf( + String.format("%03d", indexOf(habit) + 1), + habit.name, + habit.question, + habit.description, + habit.color.toCsvColor() + ) + csv.writeNext(cols, false) + } + csv.close() + } + + abstract fun resort() + enum class Order { + BY_NAME_ASC, + BY_NAME_DESC, + BY_COLOR_ASC, + BY_COLOR_DESC, + BY_SCORE_ASC, + BY_SCORE_DESC, + BY_STATUS_ASC, + BY_STATUS_DESC, + BY_POSITION + } +} 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 84945ad97..b608334b0 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 @@ -19,8 +19,6 @@ package org.isoron.uhabits.core.models import org.isoron.uhabits.core.models.Score.Companion.compute -import java.util.ArrayList -import java.util.HashMap import javax.annotation.concurrent.ThreadSafe import kotlin.math.max import kotlin.math.min @@ -139,4 +137,21 @@ class ScoreList { map[timestamp] = Score(timestamp, previousValue) } } + + @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() + map[current] = Score(current, averageScore) + current = current.minus(1) + } + } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Streak.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Streak.kt index 582dbf6f1..446f394b2 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Streak.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Streak.kt @@ -38,4 +38,8 @@ data class Streak( val length: Int get() = start.daysUntil(end) + 1 + + fun isInStreak(timestamp: Timestamp): Boolean { + return timestamp in start..end + } } 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 d3bed0af7..466e7b596 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 @@ -62,4 +62,35 @@ class StreakList { } list.add(Streak(begin, end)) } + + @Synchronized + fun isInStreaks(timestamp: Timestamp): Boolean { + return list.any { it.isInStreak(timestamp) } + } + + @Synchronized + fun combineFrom( + habitList: HabitList, + habitGroupList: HabitGroupList, + from: Timestamp, + to: Timestamp + ) { + var current = from + var streakRunning = false + var streakStart = from + while (current <= to) { + if (habitList.all { it.streaks.isInStreaks(current) } && + habitGroupList.all { it.streaks.isInStreaks(current) } && + !streakRunning + ) { + streakStart = current + streakRunning = true + } else if (streakRunning) { + val streakEnd = current.minus(1) + list.add(Streak(streakStart, streakEnd)) + streakRunning = false + } + current = current.plus(1) + } + } } From 73387e5e6309715b37d48097a985bb3926ddee1f Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sun, 26 May 2024 10:02:43 +0200 Subject: [PATCH 07/51] Creating the Habitgroup model --- .../org/isoron/uhabits/HabitsApplication.kt | 4 + .../inject/HabitsApplicationComponent.kt | 2 + .../org/isoron/uhabits/inject/HabitsModule.kt | 8 + .../org/isoron/uhabits/core/models/Habit.kt | 19 +- .../isoron/uhabits/core/models/HabitGroup.kt | 45 +++- .../uhabits/core/models/HabitGroupList.kt | 72 +++++- .../uhabits/core/models/HabitMatcher.kt | 8 + .../uhabits/core/models/ModelFactory.kt | 15 ++ .../models/memory/MemoryHabitGroupList.kt | 216 ++++++++++++++++++ .../core/models/memory/MemoryModelFactory.kt | 2 + .../core/models/sqlite/SQLModelFactory.kt | 6 + .../models/sqlite/SQLiteHabitGroupList.kt | 200 ++++++++++++++++ .../models/sqlite/records/HabitGroupRecord.kt | 100 ++++++++ .../core/models/sqlite/records/HabitRecord.kt | 10 + .../src/jvmMain/resources/migrations/26.sql | 21 +- 15 files changed, 709 insertions(+), 19 deletions(-) create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitGroupList.kt create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitGroupRecord.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt index 4f00f4d1f..da242e3d2 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt @@ -79,6 +79,10 @@ class HabitsApplication : Application() { val habitList = component.habitList for (h in habitList) h.recompute() + val habitGroupList = component.habitGroupList + for (hgr in habitGroupList) hgr.recompute() + habitGroupList.populateGroupsWith(habitList) + widgetUpdater = component.widgetUpdater.apply { startListening() scheduleStartDayWidgetUpdate() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsApplicationComponent.kt b/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsApplicationComponent.kt index da7f957f3..c3dc68e82 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsApplicationComponent.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsApplicationComponent.kt @@ -24,6 +24,7 @@ import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.io.GenericImporter import org.isoron.uhabits.core.io.Logging +import org.isoron.uhabits.core.models.HabitGroupList import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.ModelFactory import org.isoron.uhabits.core.preferences.Preferences @@ -50,6 +51,7 @@ interface HabitsApplicationComponent { val genericImporter: GenericImporter val habitCardListCache: HabitCardListCache val habitList: HabitList + val habitGroupList: HabitGroupList val intentFactory: IntentFactory val intentParser: IntentParser val logging: Logging diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsModule.kt b/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsModule.kt index c7b6843d0..bac2faa9b 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsModule.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/inject/HabitsModule.kt @@ -26,9 +26,11 @@ import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.database.Database import org.isoron.uhabits.core.database.DatabaseOpener import org.isoron.uhabits.core.io.Logging +import org.isoron.uhabits.core.models.HabitGroupList import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.ModelFactory import org.isoron.uhabits.core.models.sqlite.SQLModelFactory +import org.isoron.uhabits.core.models.sqlite.SQLiteHabitGroupList import org.isoron.uhabits.core.models.sqlite.SQLiteHabitList import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.WidgetPreferences @@ -97,6 +99,12 @@ class HabitsModule(dbFile: File) { return list } + @Provides + @AppScope + fun getHabitGroupList(list: SQLiteHabitGroupList): HabitGroupList { + return list + } + @Provides @AppScope fun getDatabaseOpener(opener: AndroidDatabaseOpener): DatabaseOpener { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index 07be68a00..fb4009046 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -40,12 +40,15 @@ data class Habit( val computedEntries: EntryList, val originalEntries: EntryList, val scores: ScoreList, - val streaks: StreakList + val streaks: StreakList, + var parentID: Long? = null, + var parentUUID: String? = null ) { init { if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "") } + var parent: HabitGroup? = null var observable = ModelObservable() val isNumerical: Boolean @@ -111,6 +114,14 @@ data class Habit( return computedEntries.getKnown().lastOrNull()?.timestamp ?: DateUtils.getTodayWithOffset() } + fun hierarchyLevel(): Int { + return if (parentID == null) { + 0 + } else { + 1 + parent!!.hierarchyLevel() + } + } + fun copyFrom(other: Habit) { this.color = other.color this.description = other.description @@ -127,6 +138,8 @@ data class Habit( this.type = other.type this.unit = other.unit this.uuid = other.uuid + this.parentID = other.parentID + this.parentUUID = other.parentUUID } override fun equals(other: Any?): Boolean { @@ -148,6 +161,8 @@ data class Habit( if (type != other.type) return false if (unit != other.unit) return false if (uuid != other.uuid) return false + if (parentID != other.parentID) return false + if (parentUUID != other.parentUUID) return false return true } @@ -168,6 +183,8 @@ data class Habit( result = 31 * result + type.value result = 31 * result + unit.hashCode() result = 31 * result + (uuid?.hashCode() ?: 0) + result = 31 * result + (parentID?.hashCode() ?: 0) + result = 31 * result + (parentUUID?.hashCode() ?: 0) return result } } 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 6d8773732..5b80f1bc7 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 @@ -12,17 +12,19 @@ data class HabitGroup( var position: Int = 0, var question: String = "", var reminder: Reminder? = null, - var unit: String = "", var uuid: String? = null, var habitList: HabitList, var habitGroupList: HabitGroupList, val scores: ScoreList, - val streaks: StreakList + val streaks: StreakList, + var parentID: Long? = null, + var parentUUID: String? = null ) { init { if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "") } + var parent: HabitGroup? = null var observable = ModelObservable() val uriString: String @@ -76,7 +78,7 @@ data class HabitGroup( ) } - fun copyFrom(other: Habit) { + fun copyFrom(other: HabitGroup) { this.color = other.color this.description = other.description // this.id should not be copied @@ -85,8 +87,9 @@ data class HabitGroup( this.position = other.position this.question = other.question this.reminder = other.reminder - this.unit = other.unit this.uuid = other.uuid + this.parentID = other.parentID + this.parentUUID = other.parentUUID } override fun equals(other: Any?): Boolean { @@ -101,8 +104,9 @@ data class HabitGroup( if (position != other.position) return false if (question != other.question) return false if (reminder != other.reminder) return false - if (unit != other.unit) return false if (uuid != other.uuid) return false + if (parentID != other.parentID) return false + if (parentUUID != other.parentUUID) return false return true } @@ -116,8 +120,37 @@ data class HabitGroup( result = 31 * result + position result = 31 * result + question.hashCode() result = 31 * result + (reminder?.hashCode() ?: 0) - result = 31 * result + unit.hashCode() result = 31 * result + (uuid?.hashCode() ?: 0) + result = 31 * result + (parentID?.hashCode() ?: 0) + result = 31 * result + (parentUUID?.hashCode() ?: 0) return result } + + fun hierarchyLevel(): Int { + return if (parentID == null) { + 0 + } else { + 1 + parent!!.hierarchyLevel() + } + } + + 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 + } } 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 583e5c82f..944adfbf3 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 @@ -1,6 +1,7 @@ package org.isoron.uhabits.core.models import com.opencsv.CSVWriter +import org.isoron.uhabits.core.models.HabitList.Order import java.io.IOException import java.io.Writer import java.util.LinkedList @@ -63,6 +64,30 @@ abstract class HabitGroupList : Iterable { */ abstract fun getByUUID(uuid: String?): HabitGroup? + /** + * Returns the habit with the specified UUID which is + * present at any hierarchy within this list. + */ + fun getHabitByUUIDDeep(uuid: String?): Habit? { + for (hgr in this) { + val habit = hgr.getHabitByUUIDDeep(uuid) + if (habit != null) { + return habit + } + } + 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. * @@ -150,6 +175,42 @@ abstract class HabitGroupList : Iterable { update(listOf(habitGroup)) } + fun populateGroupsWith(habitList: HabitList) { + val toRemove = mutableListOf() + for (habit in habitList) { + val hgr = getByUUID(habit.parentUUID) + if (hgr != null) { + hgr.habitList.add(habit) + habit.parent = hgr + toRemove.add(habit.uuid) + } + } + for (uuid in toRemove) { + val h = habitList.getByUUID(uuid) + if (h != null) { + 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() + } + } + /** * Writes the list of habits to the given writer, in CSV format. There is * one line for each habit, containing the fields name, description, @@ -186,15 +247,4 @@ abstract class HabitGroupList : Iterable { } abstract fun resort() - enum class Order { - BY_NAME_ASC, - BY_NAME_DESC, - BY_COLOR_ASC, - BY_COLOR_DESC, - BY_SCORE_ASC, - BY_SCORE_DESC, - BY_STATUS_ASC, - BY_STATUS_DESC, - BY_POSITION - } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitMatcher.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitMatcher.kt index b39e661ff..fbfedb3e0 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitMatcher.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitMatcher.kt @@ -32,6 +32,14 @@ data class HabitMatcher( return true } + fun matches(habitGroup: HabitGroup): Boolean { + if (!isArchivedAllowed && habitGroup.isArchived) return false + if (isReminderRequired && !habitGroup.hasReminder()) return false + if (!isCompletedAllowed && habitGroup.isCompletedToday()) return false + if (!isEnteredAllowed && habitGroup.isEnteredToday()) return false + return true + } + companion object { @JvmField val WITH_ALARM = HabitMatcher( 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 8c4339258..b150aa03a 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 @@ -20,6 +20,7 @@ package org.isoron.uhabits.core.models import org.isoron.uhabits.core.database.Repository import org.isoron.uhabits.core.models.sqlite.records.EntryRecord +import org.isoron.uhabits.core.models.sqlite.records.HabitGroupRecord import org.isoron.uhabits.core.models.sqlite.records.HabitRecord /** @@ -38,11 +39,25 @@ interface ModelFactory { computedEntries = buildComputedEntries() ) } + 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 + ) + } fun buildComputedEntries(): EntryList fun buildOriginalEntries(): EntryList fun buildHabitList(): HabitList + fun buildHabitGroupList(): HabitGroupList fun buildScoreList(): ScoreList fun buildStreakList(): StreakList fun buildHabitListRepository(): Repository fun buildRepetitionListRepository(): Repository + fun buildHabitGroupListRepository(): Repository } 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 new file mode 100644 index 000000000..9502fdb28 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitGroupList.kt @@ -0,0 +1,216 @@ +package org.isoron.uhabits.core.models.memory + +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.HabitGroupList +import org.isoron.uhabits.core.models.HabitList.Order +import org.isoron.uhabits.core.models.HabitMatcher +import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset +import java.util.LinkedList +import java.util.Objects + +/** + * In-memory implementation of [HabitGroupList]. + */ +class MemoryHabitGroupList : HabitGroupList { + private val list = LinkedList() + + @get:Synchronized + override var primaryOrder = Order.BY_POSITION + set(value) { + field = value + comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder) + resort() + } + + @get:Synchronized + override var secondaryOrder = Order.BY_NAME_ASC + set(value) { + field = value + comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder) + resort() + } + + private var comparator: Comparator? = + getComposedComparatorByOrder(primaryOrder, secondaryOrder) + private var parent: MemoryHabitGroupList? = null + + constructor() : super() + constructor( + matcher: HabitMatcher, + comparator: Comparator?, + parent: MemoryHabitGroupList + ) : super(matcher) { + this.parent = parent + this.comparator = comparator + primaryOrder = parent.primaryOrder + secondaryOrder = parent.secondaryOrder + parent.observable.addListener { loadFromParent() } + loadFromParent() + } + + @Synchronized + @Throws(IllegalArgumentException::class) + override fun add(habitGroup: HabitGroup) { + throwIfHasParent() + require(!list.contains(habitGroup)) { "habit already added" } + val id = habitGroup.id + if (id != null && getById(id) != null) throw RuntimeException("duplicate id") + if (id == null) habitGroup.id = list.size.toLong() + list.addLast(habitGroup) + resort() + } + + @Synchronized + override fun getById(id: Long): HabitGroup? { + for (h in list) { + checkNotNull(h.id) + if (h.id == id) return h + } + return null + } + + @Synchronized + override fun getByUUID(uuid: String?): HabitGroup? { + for (h in list) if (Objects.requireNonNull(h.uuid) == uuid) return h + return null + } + + @Synchronized + override fun getByPosition(position: Int): HabitGroup { + return list[position] + } + + @Synchronized + override fun getFiltered(matcher: HabitMatcher?): HabitGroupList { + return MemoryHabitGroupList(matcher!!, comparator, this) + } + + private fun getComposedComparatorByOrder( + firstOrder: Order, + secondOrder: Order? + ): Comparator { + return Comparator { h1: HabitGroup, h2: HabitGroup -> + val firstResult = getComparatorByOrder(firstOrder).compare(h1, h2) + if (firstResult != 0 || secondOrder == null) { + return@Comparator firstResult + } + getComparatorByOrder(secondOrder).compare(h1, h2) + } + } + + private fun getComparatorByOrder(order: Order): Comparator { + val nameComparatorAsc = Comparator { habit1, habit2 -> + habit1.name.compareTo(habit2.name) + } + val nameComparatorDesc = + Comparator { h1: HabitGroup, h2: HabitGroup -> nameComparatorAsc.compare(h2, h1) } + val colorComparatorAsc = Comparator { (color1), (color2) -> + color1.compareTo(color2) + } + val colorComparatorDesc = + Comparator { h1: HabitGroup, h2: HabitGroup -> colorComparatorAsc.compare(h2, h1) } + val scoreComparatorDesc = + Comparator { habit1, habit2 -> + val today = getTodayWithOffset() + habit1.scores[today].value.compareTo(habit2.scores[today].value) + } + val scoreComparatorAsc = + Comparator { h1: HabitGroup, h2: HabitGroup -> scoreComparatorDesc.compare(h2, h1) } + val positionComparator = + Comparator { habit1, habit2 -> habit1.position.compareTo(habit2.position) } + val statusComparatorDesc = Comparator { h1: HabitGroup, h2: HabitGroup -> + if (h1.isCompletedToday() != h2.isCompletedToday()) { + return@Comparator if (h1.isCompletedToday()) -1 else 1 + } + val today = getTodayWithOffset() + val v1 = h1.scores[today].value + val v2 = h2.scores[today].value + v2.compareTo(v1) + } + val statusComparatorAsc = + Comparator { h1: HabitGroup, h2: HabitGroup -> statusComparatorDesc.compare(h2, h1) } + return when { + order === Order.BY_POSITION -> positionComparator + order === Order.BY_NAME_ASC -> nameComparatorAsc + order === Order.BY_NAME_DESC -> nameComparatorDesc + order === Order.BY_COLOR_ASC -> colorComparatorAsc + order === Order.BY_COLOR_DESC -> colorComparatorDesc + order === Order.BY_SCORE_DESC -> scoreComparatorDesc + order === Order.BY_SCORE_ASC -> scoreComparatorAsc + order === Order.BY_STATUS_DESC -> statusComparatorDesc + order === Order.BY_STATUS_ASC -> statusComparatorAsc + else -> throw IllegalStateException() + } + } + + @Synchronized + override fun indexOf(h: HabitGroup): Int { + return list.indexOf(h) + } + + @Synchronized + override fun iterator(): Iterator { + return ArrayList(list).iterator() + } + + @Synchronized + override fun remove(h: HabitGroup) { + throwIfHasParent() + list.remove(h) + observable.notifyListeners() + } + + @Synchronized + override fun reorder(from: HabitGroup, to: HabitGroup) { + throwIfHasParent() + check(!(primaryOrder !== Order.BY_POSITION)) { "cannot reorder automatically sorted list" } + require(indexOf(from) >= 0) { "list does not contain (from) habit" } + val toPos = indexOf(to) + require(toPos >= 0) { "list does not contain (to) habit" } + list.remove(from) + list.add(toPos, from) + var position = 0 + for (h in list) h.position = position++ + observable.notifyListeners() + } + + @Synchronized + override fun size(): Int { + return list.size + } + + @Synchronized + override fun update(habitGroups: List) { + resort() + } + + private fun throwIfHasParent() { + check(parent == null) { + "Filtered lists cannot be modified directly. " + + "You should modify the parent list instead." + } + } + + @Synchronized + private fun loadFromParent() { + checkNotNull(parent) + list.clear() + for (h in parent!!) if (filter.matches(h)) list.add(h) + resort() + } + + @Synchronized + override fun resort() { + for (hgr in list) { + 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/memory/MemoryModelFactory.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryModelFactory.kt index d15f9603d..01a1dfe38 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryModelFactory.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryModelFactory.kt @@ -27,8 +27,10 @@ class MemoryModelFactory : ModelFactory { override fun buildComputedEntries() = EntryList() override fun buildOriginalEntries() = EntryList() override fun buildHabitList() = MemoryHabitList() + override fun buildHabitGroupList() = MemoryHabitGroupList() override fun buildScoreList() = ScoreList() override fun buildStreakList() = StreakList() override fun buildHabitListRepository() = throw NotImplementedError() override fun buildRepetitionListRepository() = throw NotImplementedError() + override fun buildHabitGroupListRepository() = throw NotImplementedError() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLModelFactory.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLModelFactory.kt index 096397576..3e23967cd 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLModelFactory.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLModelFactory.kt @@ -25,6 +25,7 @@ import org.isoron.uhabits.core.models.ModelFactory import org.isoron.uhabits.core.models.ScoreList import org.isoron.uhabits.core.models.StreakList import org.isoron.uhabits.core.models.sqlite.records.EntryRecord +import org.isoron.uhabits.core.models.sqlite.records.HabitGroupRecord import org.isoron.uhabits.core.models.sqlite.records.HabitRecord import javax.inject.Inject @@ -38,6 +39,8 @@ class SQLModelFactory override fun buildOriginalEntries() = SQLiteEntryList(database) override fun buildComputedEntries() = EntryList() override fun buildHabitList() = SQLiteHabitList(this) + override fun buildHabitGroupList() = SQLiteHabitGroupList(this) + override fun buildScoreList() = ScoreList() override fun buildStreakList() = StreakList() @@ -46,4 +49,7 @@ class SQLModelFactory override fun buildRepetitionListRepository() = Repository(EntryRecord::class.java, database) + + override fun buildHabitGroupListRepository() = + Repository(HabitGroupRecord::class.java, database) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt new file mode 100644 index 000000000..f9a44da65 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt @@ -0,0 +1,200 @@ +package org.isoron.uhabits.core.models.sqlite + +import org.isoron.uhabits.core.database.Repository +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.HabitGroupList +import org.isoron.uhabits.core.models.HabitList.Order +import org.isoron.uhabits.core.models.HabitMatcher +import org.isoron.uhabits.core.models.ModelFactory +import org.isoron.uhabits.core.models.memory.MemoryHabitGroupList +import org.isoron.uhabits.core.models.sqlite.records.HabitGroupRecord +import javax.inject.Inject + +/** + * Implementation of a [HabitGroupList] that is backed by SQLite. + */ +class SQLiteHabitGroupList @Inject constructor(private val modelFactory: ModelFactory) : HabitGroupList() { + private val repository: Repository = modelFactory.buildHabitGroupListRepository() + private val list: MemoryHabitGroupList = MemoryHabitGroupList() + private var loaded = false + private fun loadRecords() { + if (loaded) return + loaded = true + list.removeAll() + val records = repository.findAll("order by position") + var shouldRebuildOrder = false + for ((expectedPosition, rec) in records.withIndex()) { + if (rec.position != expectedPosition) shouldRebuildOrder = true + val h = modelFactory.buildHabitGroup() + rec.copyTo(h) + list.add(h) + } + if (shouldRebuildOrder) rebuildOrder() + } + + @Synchronized + override fun add(habitGroup: HabitGroup) { + loadRecords() + habitGroup.position = size() + val record = HabitGroupRecord() + record.copyFrom(habitGroup) + repository.save(record) + habitGroup.id = record.id + list.add(habitGroup) + observable.notifyListeners() + } + + @Synchronized + override fun getById(id: Long): HabitGroup? { + loadRecords() + return list.getById(id) + } + + @Synchronized + override fun getByUUID(uuid: String?): HabitGroup? { + loadRecords() + return list.getByUUID(uuid) + } + + @Synchronized + override fun getByPosition(position: Int): HabitGroup { + loadRecords() + return list.getByPosition(position) + } + + @Synchronized + override fun getFiltered(matcher: HabitMatcher?): HabitGroupList { + loadRecords() + return list.getFiltered(matcher) + } + + @set:Synchronized + override var primaryOrder: Order + get() = list.primaryOrder + set(order) { + list.primaryOrder = order + observable.notifyListeners() + } + + @set:Synchronized + override var secondaryOrder: Order + get() = list.secondaryOrder + set(order) { + list.secondaryOrder = order + observable.notifyListeners() + } + + @Synchronized + override fun indexOf(h: HabitGroup): Int { + loadRecords() + return list.indexOf(h) + } + + @Synchronized + override fun iterator(): Iterator { + loadRecords() + return list.iterator() + } + + @Synchronized + private fun rebuildOrder() { + val records = repository.findAll("order by position") + repository.executeAsTransaction { + for ((pos, r) in records.withIndex()) { + if (r.position != pos) { + r.position = pos + repository.save(r) + } + } + } + } + + @Synchronized + override fun remove(h: HabitGroup) { + loadRecords() + list.remove(h) + val record = repository.find( + h.id!! + ) ?: throw RuntimeException("habit not in database") + repository.executeAsTransaction { + repository.remove(record) + } + rebuildOrder() + observable.notifyListeners() + } + + @Synchronized + override fun removeAll() { + list.removeAll() + repository.execSQL("delete from habits") + repository.execSQL("delete from repetitions") + observable.notifyListeners() + } + + @Synchronized + override fun reorder(from: HabitGroup, to: HabitGroup) { + loadRecords() + list.reorder(from, to) + val fromRecord = repository.find( + from.id!! + ) + val toRecord = repository.find( + to.id!! + ) + if (fromRecord == null) throw RuntimeException("habit not in database") + if (toRecord == null) throw RuntimeException("habit not in database") + if (toRecord.position!! < fromRecord.position!!) { + repository.execSQL( + "update habits set position = position + 1 " + + "where position >= ? and position < ?", + toRecord.position!!, + fromRecord.position!! + ) + } else { + repository.execSQL( + "update habits set position = position - 1 " + + "where position > ? and position <= ?", + fromRecord.position!!, + toRecord.position!! + ) + } + fromRecord.position = toRecord.position + repository.save(fromRecord) + observable.notifyListeners() + } + + @Synchronized + override fun repair() { + loadRecords() + rebuildOrder() + observable.notifyListeners() + } + + @Synchronized + override fun size(): Int { + loadRecords() + return list.size() + } + + @Synchronized + override fun update(habitGroups: List) { + loadRecords() + list.update(habitGroups) + for (h in habitGroups) { + val record = repository.find(h.id!!) ?: continue + record.copyFrom(h) + repository.save(record) + } + observable.notifyListeners() + } + + override fun resort() { + list.resort() + observable.notifyListeners() + } + + @Synchronized + fun reload() { + loaded = false + } +} 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 new file mode 100644 index 000000000..0e5a90699 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitGroupRecord.kt @@ -0,0 +1,100 @@ +package org.isoron.uhabits.core.models.sqlite.records + +import org.isoron.uhabits.core.database.Column +import org.isoron.uhabits.core.database.Table +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 java.util.Objects.requireNonNull + +/** + * The SQLite database record corresponding to a [HabitGroup]. + */ +@Table(name = "habitgroups") +class HabitGroupRecord { + @field:Column + var description: String? = null + + @field:Column + var question: String? = null + + @field:Column + var name: String? = null + + @field:Column + var color: Int? = null + + @field:Column + var position: Int? = null + + @field:Column(name = "reminder_hour") + var reminderHour: Int? = null + + @field:Column(name = "reminder_min") + var reminderMin: Int? = null + + @field:Column(name = "reminder_days") + var reminderDays: Int? = null + + @field:Column + var highlight: Int? = null + + @field:Column + var archived: Int? = null + + @field:Column + var id: Long? = null + + @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 + description = model.description + highlight = 0 + color = model.color.paletteIndex + archived = if (model.isArchived) 1 else 0 + position = model.position + question = model.question + uuid = model.uuid + reminderDays = 0 + reminderMin = null + reminderHour = null + parentID = model.parentID + parentUUID = model.parentUUID + if (model.hasReminder()) { + val reminder = model.reminder + reminderHour = requireNonNull(reminder)!!.hour + reminderMin = reminder!!.minute + reminderDays = reminder.days.toInteger() + } + } + + fun copyTo(habitGroup: HabitGroup) { + habitGroup.id = id + habitGroup.name = name!! + habitGroup.description = description!! + habitGroup.question = question!! + habitGroup.color = PaletteColor(color!!) + 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!!, + reminderMin!!, + WeekdayList(reminderDays!!) + ) + } + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt index e4324a214..85412e5e4 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt @@ -95,6 +95,12 @@ class HabitRecord { @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: Habit) { id = model.id name = model.name @@ -109,6 +115,8 @@ class HabitRecord { position = model.position question = model.question uuid = model.uuid + parentID = model.parentID + parentUUID = model.parentUUID val (numerator, denominator) = model.frequency freqNum = numerator freqDen = denominator @@ -140,6 +148,8 @@ class HabitRecord { habit.unit = unit!! habit.position = position!! habit.uuid = uuid + habit.parentID = parentID + habit.parentUUID = parentUUID if (reminderHour != null && reminderMin != null) { habit.reminder = Reminder( reminderHour!!, diff --git a/uhabits-core/src/jvmMain/resources/migrations/26.sql b/uhabits-core/src/jvmMain/resources/migrations/26.sql index e8dc1d4a3..d934bd4b8 100644 --- a/uhabits-core/src/jvmMain/resources/migrations/26.sql +++ b/uhabits-core/src/jvmMain/resources/migrations/26.sql @@ -1,2 +1,21 @@ alter table Habits add column skip_days integer not null default 0; -alter table Habits add column skip_days_list integer not null default 0; \ No newline at end of file +alter table Habits add column skip_days_list integer not null default 0; +alter table Habits add column parent_id integer; +alter table Habits add column parent_uuid text; + +create table HabitGroups ( + id integer primary key autoincrement, + archived integer, + color integer, + description text not null default "", + highlight integer, + name text, + position integer, + reminder_days integer not null default 127, + reminder_hour integer, + reminder_min integer, + question text not null default "", + uuid text, + parent_id integer, + parent_uuid integer +); \ No newline at end of file From 262b49d025286a38a734dc9549d1ee0e93d317ba Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sun, 26 May 2024 10:03:59 +0200 Subject: [PATCH 08/51] Adding Habitgroup to type selection screen --- .../activities/habits/edit/HabitTypeDialog.kt | 6 + .../res/layout/activity_edit_habit_group.xml | 163 ++++++++++++++++++ .../src/main/res/layout/select_habit_type.xml | 19 ++ .../src/main/res/values/strings.xml | 4 + 4 files changed, 192 insertions(+) create mode 100644 uhabits-android/src/main/res/layout/activity_edit_habit_group.xml diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/HabitTypeDialog.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/HabitTypeDialog.kt index 350509207..d7acd8f03 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/HabitTypeDialog.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/HabitTypeDialog.kt @@ -51,6 +51,12 @@ class HabitTypeDialog : AppCompatDialogFragment() { dismiss() } + binding.buttonHabitGroup.setOnClickListener { + val intent = IntentFactory().startEditGroupActivity(requireActivity()) + startActivity(intent) + dismiss() + } + binding.background.setOnClickListener { dismiss() } diff --git a/uhabits-android/src/main/res/layout/activity_edit_habit_group.xml b/uhabits-android/src/main/res/layout/activity_edit_habit_group.xml new file mode 100644 index 000000000..758df4849 --- /dev/null +++ b/uhabits-android/src/main/res/layout/activity_edit_habit_group.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/uhabits-android/src/main/res/layout/select_habit_type.xml b/uhabits-android/src/main/res/layout/select_habit_type.xml index 95775cb29..dd5a8a370 100644 --- a/uhabits-android/src/main/res/layout/select_habit_type.xml +++ b/uhabits-android/src/main/res/layout/select_habit_type.xml @@ -67,6 +67,25 @@ android:text="@string/measurable_example" /> + + + + + + + diff --git a/uhabits-android/src/main/res/values/strings.xml b/uhabits-android/src/main/res/values/strings.xml index 5f632e03c..6332a1370 100644 --- a/uhabits-android/src/main/res/values/strings.xml +++ b/uhabits-android/src/main/res/values/strings.xml @@ -64,6 +64,8 @@ Off Create habit Edit habit + Create habit group + Edit habit group Check Later Welcome @@ -208,6 +210,8 @@ e.g. Did you wake up early today? Did you exercise? Did you play chess? Measurable e.g. How many miles did you run today? How many pages did you read? + Habit Group + A group to organize similar habits together. E.g. Exercise: Running, Cycling, Gym, etc. %d times per week %d times per month %d times in %d days From 832d51a0558d45a02ddd022eda1bf81df179edee Mon Sep 17 00:00:00 2001 From: Dharanish Date: Tue, 28 May 2024 19:39:53 +0200 Subject: [PATCH 09/51] 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 From 2146ce71a0de76935f854cf2f7ae2fe53fc68c4d Mon Sep 17 00:00:00 2001 From: Dharanish Date: Tue, 25 Jun 2024 11:46:48 +0200 Subject: [PATCH 10/51] Revert "Skip days implemented. Scores not correct yet" This reverts commit ec08b602 --- .../habits/edit/EditHabitActivity.kt | 43 ------------ .../main/res/layout/activity_edit_habit.xml | 20 ------ .../src/main/res/values/strings.xml | 2 - .../assets/main/migrations/009.sql | 2 +- .../isoron/uhabits/models/HabitRepository.kt | 36 +++++----- .../isoron/uhabits/core/models/EntryList.kt | 69 +++++-------------- .../org/isoron/uhabits/core/models/Habit.kt | 31 +-------- .../isoron/uhabits/core/models/ScoreList.kt | 20 +----- .../isoron/uhabits/core/models/WeekdayList.kt | 12 ---- .../core/models/sqlite/SQLiteEntryList.kt | 12 ++-- .../core/models/sqlite/records/HabitRecord.kt | 20 ------ .../screens/habits/list/HabitCardListCache.kt | 3 +- .../screens/habits/list/ListHabitsBehavior.kt | 5 +- 13 files changed, 49 insertions(+), 226 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt index 482514e2a..a2f329d71 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt @@ -51,7 +51,6 @@ import org.isoron.uhabits.core.models.HabitType import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.Reminder -import org.isoron.uhabits.core.models.SkipDays import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.databinding.ActivityEditHabitBinding import org.isoron.uhabits.utils.ColorUtils @@ -82,8 +81,6 @@ class EditHabitActivity : AppCompatActivity() { var androidColor = 0 var freqNum = 1 var freqDen = 1 - var isSkipDays = false - var listSkipDays: WeekdayList = WeekdayList.NO_DAY var reminderHour = -1 var reminderMin = -1 var reminderDays: WeekdayList = WeekdayList.EVERY_DAY @@ -107,8 +104,6 @@ class EditHabitActivity : AppCompatActivity() { color = habit.color freqNum = habit.frequency.numerator freqDen = habit.frequency.denominator - isSkipDays = habit.skipDays.isSkipDays - listSkipDays = habit.skipDays.days targetType = habit.targetType habit.reminder?.let { reminderHour = it.hour @@ -130,8 +125,6 @@ class EditHabitActivity : AppCompatActivity() { color = PaletteColor(state.getInt("paletteColor")) freqNum = state.getInt("freqNum") freqDen = state.getInt("freqDen") - isSkipDays = state.getBoolean("isSkipDays", false) - listSkipDays = WeekdayList(state.getInt("listSkipDays", 0)) reminderHour = state.getInt("reminderHour") reminderMin = state.getInt("reminderMin") reminderDays = WeekdayList(state.getInt("reminderDays")) @@ -248,31 +241,12 @@ class EditHabitActivity : AppCompatActivity() { dialog.setListener { days: WeekdayList -> reminderDays = days if (reminderDays.isEmpty) reminderDays = WeekdayList.EVERY_DAY - if (isSkipDays) reminderDays = WeekdayList(reminderDays.toArray(), listSkipDays.toArray()) populateReminder() } dialog.setSelectedDays(reminderDays) dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker") } - populateSkipDays() - binding.skipDaysPicker.setOnClickListener { - val dialog = WeekdayPickerDialog() - - dialog.setListener { days: WeekdayList -> - listSkipDays = days - if (listSkipDays.isEmpty) listSkipDays = WeekdayList.NO_DAY - isSkipDays = (listSkipDays != WeekdayList.NO_DAY) - if (reminderHour >= 0 && isSkipDays) { - reminderDays = WeekdayList(reminderDays.toArray(), listSkipDays.toArray()) - populateReminder() - } - populateSkipDays() - } - dialog.setSelectedDays(listSkipDays) - dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker") - } - binding.buttonSave.setOnClickListener { if (validate()) save() } @@ -303,7 +277,6 @@ class EditHabitActivity : AppCompatActivity() { } habit.frequency = Frequency(freqNum, freqDen) - habit.skipDays = SkipDays(isSkipDays, listSkipDays) if (habitType == HabitType.NUMERICAL) { habit.targetValue = binding.targetInput.text.toString().toDouble() habit.targetType = targetType @@ -357,20 +330,6 @@ class EditHabitActivity : AppCompatActivity() { } } - private fun populateSkipDays() { - val preferences = (application as HabitsApplication).component.preferences - if (preferences.isSkipEnabled || isSkipDays) { - binding.skipDaysOuterBox.visibility = View.VISIBLE - } else { - binding.skipDaysOuterBox.visibility = View.GONE - } - if (isSkipDays) { - binding.skipDaysPicker.text = listSkipDays.toFormattedString(this) - } else { - binding.skipDaysPicker.text = getString(R.string.skip_days_off) - } - } - @SuppressLint("StringFormatMatches") private fun populateFrequency() { binding.booleanFrequencyPicker.text = formatFrequency(freqNum, freqDen, resources) @@ -413,8 +372,6 @@ class EditHabitActivity : AppCompatActivity() { putInt("androidColor", androidColor) putInt("freqNum", freqNum) putInt("freqDen", freqDen) - putBoolean("isSkipDays", isSkipDays) - putInt("listSkipDays", listSkipDays.toInteger()) putInt("reminderHour", reminderHour) putInt("reminderMin", reminderMin) putInt("reminderDays", reminderDays.toInteger()) diff --git a/uhabits-android/src/main/res/layout/activity_edit_habit.xml b/uhabits-android/src/main/res/layout/activity_edit_habit.xml index b001f8105..75dddb551 100644 --- a/uhabits-android/src/main/res/layout/activity_edit_habit.xml +++ b/uhabits-android/src/main/res/layout/activity_edit_habit.xml @@ -73,7 +73,6 @@ android:paddingRight="4dp"> - - - - - - - - - - - History Clear Reminder - Skip days Save Streaks You have no active habits You\'re all done for today! Press-and-hold to check or uncheck Off - Off Create habit Edit habit Create habit group diff --git a/uhabits-core-legacy/assets/main/migrations/009.sql b/uhabits-core-legacy/assets/main/migrations/009.sql index 1fae1a722..5a4afd962 100644 --- a/uhabits-core-legacy/assets/main/migrations/009.sql +++ b/uhabits-core-legacy/assets/main/migrations/009.sql @@ -1,4 +1,4 @@ -create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer highlight integer, name text, position integer, reminder_hour integer, reminder_min integer ) +create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer, highlight integer, name text, position integer, reminder_hour integer, reminder_min integer ) create table Checkmarks ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer, value integer ) create table Repetitions ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer ) create table Streak ( id integer primary key autoincrement, end integer, habit integer references habits(id), length integer, start integer ) diff --git a/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt b/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt index 20e86fcbe..961956f51 100644 --- a/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt +++ b/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt @@ -28,9 +28,9 @@ import org.isoron.platform.io.nextId class HabitRepository(var db: Database) { companion object { - const val SELECT_COLUMNS = "id, name, description, freq_num, freq_den, skip_days, skip_days_list, color, archived, position, unit, target_value, type" - const val SELECT_PLACEHOLDERS = "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?" - const val UPDATE_COLUMNS = "id=?, name=?, description=?, freq_num=?, freq_den=?, skip_days=?, skip_days_list=?, color=?, archived=?, position=?, unit=?, target_value=?, type=?" + const val SELECT_COLUMNS = "id, name, description, freq_num, freq_den, color, archived, position, unit, target_value, type" + const val SELECT_PLACEHOLDERS = "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?" + const val UPDATE_COLUMNS = "id=?, name=?, description=?, freq_num=?, freq_den=?, color=?, archived=?, position=?, unit=?, target_value=?, type=?" } private val findAllStatement = db.prepareStatement("select $SELECT_COLUMNS from habits order by position") @@ -60,7 +60,7 @@ class HabitRepository(var db: Database) { fun update(habit: Habit) { bindHabitToStatement(habit, updateStatement) - updateStatement.bindInt(13, habit.id) + updateStatement.bindInt(11, habit.id) updateStatement.step() updateStatement.reset() } @@ -70,14 +70,12 @@ class HabitRepository(var db: Database) { name = stmt.getText(1), description = stmt.getText(2), frequency = Frequency(stmt.getInt(3), stmt.getInt(4)), - skipDays = (stmt.getInt(5) == 1), - skipDaysList = WeekDayList(stmt.getInt(6)), - color = PaletteColor(stmt.getInt(7)), - isArchived = stmt.getInt(8) != 0, - position = stmt.getInt(9), - unit = stmt.getText(10), - target = stmt.getReal(11), - type = if (stmt.getInt(12) == 0) HabitType.BOOLEAN_HABIT else HabitType.NUMERICAL_HABIT) + color = PaletteColor(stmt.getInt(5)), + isArchived = stmt.getInt(6) != 0, + position = stmt.getInt(7), + unit = stmt.getText(8), + target = stmt.getReal(9), + type = if (stmt.getInt(10) == 0) HabitType.BOOLEAN_HABIT else HabitType.NUMERICAL_HABIT) } private fun bindHabitToStatement(habit: Habit, statement: PreparedStatement) { @@ -86,14 +84,12 @@ class HabitRepository(var db: Database) { statement.bindText(2, habit.description) statement.bindInt(3, habit.frequency.numerator) statement.bindInt(4, habit.frequency.denominator) - statement.bindInt(5, if (habit.skipDays) 1 else 0) - statement.bindInt(6, habit.skipDaysList.toInteger()) - statement.bindInt(7, habit.color.index) - statement.bindInt(8, if (habit.isArchived) 1 else 0) - statement.bindInt(9, habit.position) - statement.bindText(10, habit.unit) - statement.bindReal(11, habit.target) - statement.bindInt(12, habit.type.code) + statement.bindInt(5, habit.color.index) + statement.bindInt(6, if (habit.isArchived) 1 else 0) + statement.bindInt(7, habit.position) + statement.bindText(8, habit.unit) + statement.bindReal(9, habit.target) + statement.bindInt(10, habit.type.code) } fun delete(habit: Habit) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt index 12ba3dd2a..5c5499f49 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt @@ -38,12 +38,11 @@ open class EntryList { /** * Returns the entry corresponding to the given timestamp. If no entry with such timestamp - * has been previously added, returns Entry(timestamp, UNKNOWN). or Entry(timestamp, SKIP) if - * skip days are enabled and that day is to be skipped + * has been previously added, returns Entry(timestamp, UNKNOWN). */ @Synchronized - open fun get(timestamp: Timestamp, skipDays: SkipDays = SkipDays.NONE): Entry { - return if (skipDays.isDaySkipped(timestamp)) Entry(timestamp, SKIP) else entriesByTimestamp[timestamp] ?: Entry(timestamp, UNKNOWN) + open fun get(timestamp: Timestamp): Entry { + return entriesByTimestamp[timestamp] ?: Entry(timestamp, UNKNOWN) } /** @@ -52,12 +51,12 @@ open class EntryList { * included. */ @Synchronized - open fun getByInterval(from: Timestamp, to: Timestamp, skipDays: SkipDays = SkipDays.NONE): List { + open fun getByInterval(from: Timestamp, to: Timestamp): List { val result = mutableListOf() if (from.isNewerThan(to)) return result var current = to while (current >= from) { - result.add(get(current, skipDays)) + result.add(get(current)) current = current.minus(1) } return result @@ -91,18 +90,16 @@ open class EntryList { open fun recomputeFrom( originalEntries: EntryList, frequency: Frequency, - isNumerical: Boolean, - skipDays: SkipDays + isNumerical: Boolean ) { clear() val original = originalEntries.getKnown() if (isNumerical) { - val computed = addEntriesWithSkipDays(original, skipDays) - computed.forEach { add(it) } + original.forEach { add(it) } } else { val intervals = buildIntervals(frequency, original) snapIntervalsTogether(intervals) - val computed = buildEntriesFromInterval(original, intervals, skipDays) + val computed = buildEntriesFromInterval(original, intervals) computed.filter { it.value != UNKNOWN || it.notes.isNotEmpty() }.forEach { add(it) } } } @@ -131,10 +128,10 @@ open class EntryList { fun computeWeekdayFrequency(isNumerical: Boolean): HashMap> { val entries = getKnown() val map = hashMapOf>() - for ((computedTimestamp, value) in entries) { - val weekday = computedTimestamp.weekday + for ((originalTimestamp, value) in entries) { + val weekday = originalTimestamp.weekday val truncatedTimestamp = Timestamp( - computedTimestamp.toCalendar().apply { + originalTimestamp.toCalendar().apply { set(Calendar.DAY_OF_MONTH, 1) }.timeInMillis ) @@ -145,7 +142,7 @@ open class EntryList { map[truncatedTimestamp] = list } - if (isNumerical && value != SKIP) { + if (isNumerical) { list[weekday] += value } else if (value == YES_MANUAL) { list[weekday] += 1 @@ -170,8 +167,7 @@ open class EntryList { */ fun buildEntriesFromInterval( original: List, - intervals: List, - skipDays: SkipDays + intervals: List ): List { val result = arrayListOf() if (original.isEmpty()) return result @@ -200,17 +196,15 @@ open class EntryList { current = interval.end while (current >= interval.begin) { val offset = current.daysUntil(to) - result[offset] = if (skipDays.isDaySkipped(current)) Entry(current, SKIP) else Entry(current, YES_AUTO) + result[offset] = Entry(current, YES_AUTO) current = current.minus(1) } } - // Copy original entries except for the skipped days + // Copy original entries original.forEach { entry -> val offset = entry.timestamp.daysUntil(to) - val value = if (skipDays.isDaySkipped(entry)) { - SKIP - } else if ( + val value = if ( result[offset].value == UNKNOWN || entry.value == SKIP || entry.value == YES_MANUAL @@ -277,29 +271,6 @@ open class EntryList { } return intervals } - - fun addEntriesWithSkipDays(original: List, skipDays: SkipDays): List { - if (original.isEmpty()) return original - val earliest = original.last().timestamp - val today = DateUtils.getTodayWithOffset() - val computed = mutableListOf() - var current = today - var offset = 0 - while (current >= earliest) { - if (current == original[offset].timestamp) { - if (!skipDays.isDaySkipped(current)) { - computed.add(original[offset]) - } else { - computed.add(Entry(current, SKIP)) - } - offset++ - } else if (skipDays.isDaySkipped(current)) { - computed.add(Entry(current, SKIP)) - } - current = current.minus(1) - } - return computed - } } } @@ -352,12 +323,10 @@ fun List.groupedSum( */ fun List.countSkippedDays( truncateField: DateUtils.TruncateField, - firstWeekday: Int = Calendar.SATURDAY, - skipDays: SkipDays + firstWeekday: Int = Calendar.SATURDAY ): List { - val thisIntervalStart = DateUtils.getTodayWithOffset().truncate(truncateField, firstWeekday) return this.map { (timestamp, value) -> - if (value == SKIP || skipDays.isDaySkipped(timestamp)) { + if (value == SKIP) { Entry(timestamp, 1) } else { Entry(timestamp, 0) @@ -371,5 +340,5 @@ fun List.countSkippedDays( Entry(timestamp, entries.sumOf { it.value }) }.sortedBy { (timestamp, _) -> -timestamp.unixTime - }.filter { it.timestamp == thisIntervalStart } + } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index fb4009046..a06d01ec9 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -25,7 +25,6 @@ data class Habit( var color: PaletteColor = PaletteColor(8), var description: String = "", var frequency: Frequency = Frequency.DAILY, - var skipDays: SkipDays = SkipDays.NONE, var id: Long? = null, var isArchived: Boolean = false, var name: String = "", @@ -40,15 +39,12 @@ data class Habit( val computedEntries: EntryList, val originalEntries: EntryList, val scores: ScoreList, - val streaks: StreakList, - var parentID: Long? = null, - var parentUUID: String? = null + val streaks: StreakList ) { init { if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "") } - var parent: HabitGroup? = null var observable = ModelObservable() val isNumerical: Boolean @@ -82,8 +78,7 @@ data class Habit( computedEntries.recomputeFrom( originalEntries = originalEntries, frequency = frequency, - isNumerical = isNumerical, - skipDays = skipDays + isNumerical = isNumerical ) val today = DateUtils.getTodayWithOffset() @@ -95,7 +90,6 @@ data class Habit( scores.recompute( frequency = frequency, isNumerical = isNumerical, - skipDays = skipDays, numericalHabitType = targetType, targetValue = targetValue, computedEntries = computedEntries, @@ -110,23 +104,10 @@ data class Habit( ) } - fun firstEntryDate(): Timestamp { - return computedEntries.getKnown().lastOrNull()?.timestamp ?: DateUtils.getTodayWithOffset() - } - - fun hierarchyLevel(): Int { - return if (parentID == null) { - 0 - } else { - 1 + parent!!.hierarchyLevel() - } - } - fun copyFrom(other: Habit) { this.color = other.color this.description = other.description this.frequency = other.frequency - this.skipDays = other.skipDays // this.id should not be copied this.isArchived = other.isArchived this.name = other.name @@ -138,8 +119,6 @@ data class Habit( this.type = other.type this.unit = other.unit this.uuid = other.uuid - this.parentID = other.parentID - this.parentUUID = other.parentUUID } override fun equals(other: Any?): Boolean { @@ -149,7 +128,6 @@ data class Habit( if (color != other.color) return false if (description != other.description) return false if (frequency != other.frequency) return false - if (skipDays != other.skipDays) return false if (id != other.id) return false if (isArchived != other.isArchived) return false if (name != other.name) return false @@ -161,8 +139,6 @@ data class Habit( if (type != other.type) return false if (unit != other.unit) return false if (uuid != other.uuid) return false - if (parentID != other.parentID) return false - if (parentUUID != other.parentUUID) return false return true } @@ -171,7 +147,6 @@ data class Habit( var result = color.hashCode() result = 31 * result + description.hashCode() result = 31 * result + frequency.hashCode() - result = 31 * result + skipDays.hashCode() result = 31 * result + (id?.hashCode() ?: 0) result = 31 * result + isArchived.hashCode() result = 31 * result + name.hashCode() @@ -183,8 +158,6 @@ data class Habit( result = 31 * result + type.value result = 31 * result + unit.hashCode() result = 31 * result + (uuid?.hashCode() ?: 0) - result = 31 * result + (parentID?.hashCode() ?: 0) - result = 31 * result + (parentUUID?.hashCode() ?: 0) return result } } 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 dc99c0677..3acc19e1c 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 @@ -19,6 +19,8 @@ package org.isoron.uhabits.core.models import org.isoron.uhabits.core.models.Score.Companion.compute +import java.util.ArrayList +import java.util.HashMap import javax.annotation.concurrent.ThreadSafe import kotlin.math.max import kotlin.math.min @@ -66,7 +68,6 @@ class ScoreList { fun recompute( frequency: Frequency, isNumerical: Boolean, - skipDays: SkipDays, numericalHabitType: NumericalHabitType, targetValue: Double, computedEntries: EntryList, @@ -78,7 +79,7 @@ class ScoreList { var numerator = frequency.numerator var denominator = frequency.denominator val freq = frequency.toDouble() - val values = computedEntries.getByInterval(from, to, skipDays).map { it.value }.toIntArray() + val values = computedEntries.getByInterval(from, to).map { it.value }.toIntArray() val isAtMost = numericalHabitType == NumericalHabitType.AT_MOST // For non-daily boolean habits, we double the numerator and the denominator to smooth @@ -137,19 +138,4 @@ class ScoreList { map[timestamp] = Score(timestamp, previousValue) } } - - @Synchronized - fun combineFrom( - habitList: HabitList, - from: Timestamp, - to: Timestamp - ) { - var current = to - while (current >= from) { - val habitScores = habitList.map { it.scores[current].value } - 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/WeekdayList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/WeekdayList.kt index 1430a7dc9..55dfa3b9b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/WeekdayList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/WeekdayList.kt @@ -38,13 +38,6 @@ class WeekdayList { this.weekdays = Arrays.copyOf(weekdays, 7) } - constructor(addDays: BooleanArray, removeDays: BooleanArray) { - weekdays = BooleanArray(7) - for (i in 0..6) { - weekdays[i] = addDays[i] && !removeDays[i] - } - } - val isEmpty: Boolean get() { for (d in weekdays) if (d) return false @@ -65,10 +58,6 @@ class WeekdayList { return packedList } - fun isDayTrue(dayNum: Int): Boolean { - return weekdays[dayNum] - } - override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false @@ -84,6 +73,5 @@ class WeekdayList { companion object { val EVERY_DAY = WeekdayList(127) - val NO_DAY = WeekdayList(booleanArrayOf(false, false, false, false, false, false, false)) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt index 824234ad2..128accd13 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt @@ -24,7 +24,6 @@ import org.isoron.uhabits.core.database.Repository import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.EntryList import org.isoron.uhabits.core.models.Frequency -import org.isoron.uhabits.core.models.SkipDays import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.sqlite.records.EntryRecord @@ -44,13 +43,14 @@ class SQLiteEntryList(database: Database) : EntryList() { isLoaded = true } - override fun get(timestamp: Timestamp, skipDays: SkipDays): Entry { + override fun get(timestamp: Timestamp): Entry { loadRecords() - return super.get(timestamp, skipDays) + return super.get(timestamp) } - override fun getByInterval(from: Timestamp, to: Timestamp, skipDays: SkipDays): List { + + override fun getByInterval(from: Timestamp, to: Timestamp): List { loadRecords() - return super.getByInterval(from, to, skipDays) + return super.getByInterval(from, to) } override fun add(entry: Entry) { @@ -78,7 +78,7 @@ class SQLiteEntryList(database: Database) : EntryList() { return super.getKnown() } - override fun recomputeFrom(originalEntries: EntryList, frequency: Frequency, isNumerical: Boolean, skipDays: SkipDays) { + override fun recomputeFrom(originalEntries: EntryList, frequency: Frequency, isNumerical: Boolean) { throw UnsupportedOperationException() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt index 85412e5e4..dc0386799 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt @@ -26,7 +26,6 @@ import org.isoron.uhabits.core.models.HabitType import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.Reminder -import org.isoron.uhabits.core.models.SkipDays import org.isoron.uhabits.core.models.WeekdayList import java.util.Objects.requireNonNull @@ -50,12 +49,6 @@ class HabitRecord { @field:Column(name = "freq_den") var freqDen: Int? = null - @field:Column(name = "skip_days") - var skipDays: Int? = null - - @field:Column(name = "skip_days_list") - var skipDaysList: Int? = null - @field:Column var color: Int? = null @@ -95,12 +88,6 @@ class HabitRecord { @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: Habit) { id = model.id name = model.name @@ -115,13 +102,9 @@ class HabitRecord { position = model.position question = model.question uuid = model.uuid - parentID = model.parentID - parentUUID = model.parentUUID val (numerator, denominator) = model.frequency freqNum = numerator freqDen = denominator - skipDays = if (model.skipDays.isSkipDays) 1 else 0 - skipDaysList = model.skipDays.days.toInteger() reminderDays = 0 reminderMin = null reminderHour = null @@ -139,7 +122,6 @@ class HabitRecord { habit.description = description!! habit.question = question!! habit.frequency = Frequency(freqNum!!, freqDen!!) - habit.skipDays = SkipDays(skipDays!! == 1, WeekdayList(skipDaysList!!)) habit.color = PaletteColor(color!!) habit.isArchived = archived != 0 habit.type = HabitType.fromInt(type!!) @@ -148,8 +130,6 @@ class HabitRecord { habit.unit = unit!! habit.position = position!! habit.uuid = uuid - habit.parentID = parentID - habit.parentUUID = parentUUID if (reminderHour != null && reminderMin != null) { habit.reminder = Reminder( reminderHour!!, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index 6615dcf60..c7d861813 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -311,8 +311,7 @@ class HabitCardListCache @Inject constructor( newData.scores[habit.id] = habit.scores[today].value val list: MutableList = ArrayList() val notes: MutableList = ArrayList() - val skipDays = habit.skipDays - for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today, skipDays)) { + for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) { list.add(value) notes.add(note) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index 703a8cf0a..b66b08be6 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -20,7 +20,6 @@ package org.isoron.uhabits.core.ui.screens.habits.list import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateRepetitionCommand -import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitList @@ -54,12 +53,10 @@ open class ListHabitsBehavior @Inject constructor( fun onEdit(habit: Habit, timestamp: Timestamp?) { val entry = habit.computedEntries.get(timestamp!!) - if (habit.skipDays.isDaySkipped(timestamp)) return if (habit.type == HabitType.NUMERICAL) { val oldValue = entry.value.toDouble() / 1000 screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String, x: Float, y: Float -> - val value = if (habit.skipDays.isDaySkipped(timestamp)) Entry.SKIP else (newValue * 1000).roundToInt() - + val value = (newValue * 1000).roundToInt() if (newValue != oldValue) { if ( (habit.targetType == AT_LEAST && newValue >= habit.targetValue) || From 08113a57ace0f875a8d9c839c7a48d8896f4f6b4 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Tue, 25 Jun 2024 12:06:40 +0200 Subject: [PATCH 11/51] Remove SkipDays feature --- .../habits/show/views/SubtitleCardView.kt | 7 ---- .../org/isoron/uhabits/core/models/Habit.kt | 23 ++++++++++- .../isoron/uhabits/core/models/ScoreList.kt | 17 +++++++- .../isoron/uhabits/core/models/SkipDays.kt | 41 ------------------- .../core/models/sqlite/records/HabitRecord.kt | 10 +++++ .../screens/habits/show/views/HistoryCard.kt | 4 +- .../screens/habits/show/views/SubtitleCard.kt | 3 -- .../screens/habits/show/views/TargetCard.kt | 40 ++++++++---------- 8 files changed, 64 insertions(+), 81 deletions(-) delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/SkipDays.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCardView.kt index 5e49755be..c29894c18 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCardView.kt @@ -33,7 +33,6 @@ import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding import org.isoron.uhabits.utils.InterfaceUtils import org.isoron.uhabits.utils.formatTime -import org.isoron.uhabits.utils.toFormattedString class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { @@ -79,12 +78,6 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con if (state.question.isEmpty()) { binding.questionLabel.visibility = View.GONE } - if (state.skipDays.isSkipDays) { - binding.skipLabel.visibility = View.VISIBLE - binding.skipLabel.text = context.getString(R.string.skip_day) + " " + state.skipDays.days.toFormattedString(context) - } else { - binding.skipLabel.visibility = View.GONE - } postInvalidate() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index a06d01ec9..ff57e823e 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -39,12 +39,15 @@ data class Habit( val computedEntries: EntryList, val originalEntries: EntryList, val scores: ScoreList, - val streaks: StreakList + val streaks: StreakList, + var parentID: Long? = null, + var parentUUID: String? = null ) { init { if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "") } + var parent: HabitGroup? = null var observable = ModelObservable() val isNumerical: Boolean @@ -104,6 +107,18 @@ data class Habit( ) } + fun firstEntryDate(): Timestamp { + return computedEntries.getKnown().lastOrNull()?.timestamp ?: DateUtils.getTodayWithOffset() + } + + fun hierarchyLevel(): Int { + return if (parentID == null) { + 0 + } else { + 1 + parent!!.hierarchyLevel() + } + } + fun copyFrom(other: Habit) { this.color = other.color this.description = other.description @@ -119,6 +134,8 @@ data class Habit( this.type = other.type this.unit = other.unit this.uuid = other.uuid + this.parentID = other.parentID + this.parentUUID = other.parentUUID } override fun equals(other: Any?): Boolean { @@ -139,6 +156,8 @@ data class Habit( if (type != other.type) return false if (unit != other.unit) return false if (uuid != other.uuid) return false + if (parentID != other.parentID) return false + if (parentUUID != other.parentUUID) return false return true } @@ -158,6 +177,8 @@ data class Habit( result = 31 * result + type.value result = 31 * result + unit.hashCode() result = 31 * result + (uuid?.hashCode() ?: 0) + result = 31 * result + (parentID?.hashCode() ?: 0) + result = 31 * result + (parentUUID?.hashCode() ?: 0) return result } } 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 3acc19e1c..bc6d139a1 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 @@ -19,8 +19,6 @@ package org.isoron.uhabits.core.models import org.isoron.uhabits.core.models.Score.Companion.compute -import java.util.ArrayList -import java.util.HashMap import javax.annotation.concurrent.ThreadSafe import kotlin.math.max import kotlin.math.min @@ -138,4 +136,19 @@ class ScoreList { map[timestamp] = Score(timestamp, previousValue) } } + + @Synchronized + fun combineFrom( + habitList: HabitList, + from: Timestamp, + to: Timestamp + ) { + var current = to + while (current >= from) { + val habitScores = habitList.map { it.scores[current].value } + 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/SkipDays.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/SkipDays.kt deleted file mode 100644 index 4d386fe59..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/SkipDays.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ -package org.isoron.uhabits.core.models - -data class SkipDays( - val isSkipDays: Boolean, - val days: WeekdayList -) { - fun isDaySkipped(day: Int): Boolean { - return isSkipDays && days.isDayTrue(day) - } - - fun isDaySkipped(day: Timestamp): Boolean { - return isSkipDays && days.isDayTrue(day.weekday) - } - - fun isDaySkipped(entry: Entry): Boolean { - return isSkipDays && days.isDayTrue(entry.timestamp.weekday) - } - - companion object { - @JvmField - val NONE = SkipDays(false, WeekdayList(0)) - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt index dc0386799..0a61f65f0 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt @@ -88,6 +88,12 @@ class HabitRecord { @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: Habit) { id = model.id name = model.name @@ -102,6 +108,8 @@ class HabitRecord { position = model.position question = model.question uuid = model.uuid + parentID = model.parentID + parentUUID = model.parentUUID val (numerator, denominator) = model.frequency freqNum = numerator freqDen = denominator @@ -130,6 +138,8 @@ class HabitRecord { habit.unit = unit!! habit.position = position!! habit.uuid = uuid + habit.parentID = parentID + habit.parentUUID = parentUUID if (reminderHour != null && reminderMin != null) { habit.reminder = Reminder( reminderHour!!, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt index 73f7be416..0a28c801f 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt @@ -67,7 +67,6 @@ class HistoryCardPresenter( override fun onDateLongPress(date: LocalDate) { val timestamp = Timestamp.fromLocalDate(date) screen.showFeedback() - if (habit.skipDays.isDaySkipped(timestamp)) return if (habit.isNumerical) { showNumberPopup(timestamp) } else { @@ -82,7 +81,6 @@ class HistoryCardPresenter( override fun onDateShortPress(date: LocalDate) { val timestamp = Timestamp.fromLocalDate(date) screen.showFeedback() - if (habit.skipDays.isDaySkipped(timestamp)) return if (habit.isNumerical) { showNumberPopup(timestamp) } else { @@ -163,7 +161,7 @@ class HistoryCardPresenter( ): HistoryCardState { val today = DateUtils.getTodayWithOffset() val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today - val entries = habit.computedEntries.getByInterval(oldest, today, habit.skipDays) + val entries = habit.computedEntries.getByInterval(oldest, today) val series = if (habit.isNumerical) { entries.map { when { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt index 01784e2f2..fb839933c 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt @@ -24,14 +24,12 @@ import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.Reminder -import org.isoron.uhabits.core.models.SkipDays import org.isoron.uhabits.core.ui.views.Theme data class SubtitleCardState( val color: PaletteColor, val frequency: Frequency, val isNumerical: Boolean, - val skipDays: SkipDays, val question: String, val reminder: Reminder?, val targetValue: Double = 0.0, @@ -49,7 +47,6 @@ class SubtitleCardPresenter { color = habit.color, frequency = habit.frequency, isNumerical = habit.isNumerical, - skipDays = habit.skipDays, question = habit.question, reminder = habit.reminder, targetValue = habit.targetValue, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt index 43a240dc6..f137de23e 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt @@ -26,7 +26,6 @@ import org.isoron.uhabits.core.models.countSkippedDays import org.isoron.uhabits.core.models.groupedSum import org.isoron.uhabits.core.ui.views.Theme import org.isoron.uhabits.core.utils.DateUtils -import java.util.ArrayList import java.util.Calendar import kotlin.math.max @@ -46,61 +45,54 @@ class TargetCardPresenter { theme: Theme ): TargetCardState { val today = DateUtils.getTodayWithOffset() - val (yearBegin, yearEnd) = getYearRange(firstWeekday) val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today - val entriesForSkip = habit.computedEntries.getByInterval(yearBegin, yearEnd, habit.skipDays) - val entriesForSum = habit.computedEntries.getByInterval(oldest, today) + val entries = habit.computedEntries.getByInterval(oldest, today) - val valueToday = entriesForSum.groupedSum( + val valueToday = entries.groupedSum( truncateField = DateUtils.TruncateField.DAY, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 - val skippedDayToday = entriesForSkip.countSkippedDays( - truncateField = DateUtils.TruncateField.DAY, - skipDays = habit.skipDays + val skippedDayToday = entries.countSkippedDays( + truncateField = DateUtils.TruncateField.DAY ).firstOrNull()?.value ?: 0 - val valueThisWeek = entriesForSum.groupedSum( + val valueThisWeek = entries.groupedSum( truncateField = DateUtils.TruncateField.WEEK_NUMBER, firstWeekday = firstWeekday, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 - val skippedDaysThisWeek = entriesForSkip.countSkippedDays( + val skippedDaysThisWeek = entries.countSkippedDays( truncateField = DateUtils.TruncateField.WEEK_NUMBER, - firstWeekday = firstWeekday, - skipDays = habit.skipDays + firstWeekday = firstWeekday ).firstOrNull()?.value ?: 0 - val valueThisMonth = entriesForSum.groupedSum( + val valueThisMonth = entries.groupedSum( truncateField = DateUtils.TruncateField.MONTH, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 - val skippedDaysThisMonth = entriesForSkip.countSkippedDays( - truncateField = DateUtils.TruncateField.MONTH, - skipDays = habit.skipDays + val skippedDaysThisMonth = entries.countSkippedDays( + truncateField = DateUtils.TruncateField.MONTH ).firstOrNull()?.value ?: 0 - val valueThisQuarter = entriesForSum.groupedSum( + val valueThisQuarter = entries.groupedSum( truncateField = DateUtils.TruncateField.QUARTER, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 - val skippedDaysThisQuarter = entriesForSkip.countSkippedDays( - truncateField = DateUtils.TruncateField.QUARTER, - skipDays = habit.skipDays + val skippedDaysThisQuarter = entries.countSkippedDays( + truncateField = DateUtils.TruncateField.QUARTER ).firstOrNull()?.value ?: 0 - val valueThisYear = entriesForSum.groupedSum( + val valueThisYear = entries.groupedSum( truncateField = DateUtils.TruncateField.YEAR, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 - val skippedDaysThisYear = entriesForSkip.countSkippedDays( - truncateField = DateUtils.TruncateField.YEAR, - skipDays = habit.skipDays + val skippedDaysThisYear = entries.countSkippedDays( + truncateField = DateUtils.TruncateField.YEAR ).firstOrNull()?.value ?: 0 val cal = DateUtils.getStartOfTodayCalendarWithOffset() From af3283e52ff6cae0acbf691149e09c9b13f511bd Mon Sep 17 00:00:00 2001 From: Dharanish Date: Wed, 26 Jun 2024 22:00:02 +0200 Subject: [PATCH 12/51] Can create habit groups now --- uhabits-android/src/main/AndroidManifest.xml | 8 + .../habits/list/views/HabitCardListAdapter.kt | 2 +- .../habits/list/views/HabitGroupCardView.kt | 160 ++++++++++++++++++ .../list/views/HabitGroupCardViewHolder.kt | 5 + .../commands/ArchiveHabitGroupsCommand.kt | 19 +++ .../commands/ChangeHabitGroupColorCommand.kt | 16 ++ .../core/commands/DeleteHabitGroupsCommand.kt | 13 ++ .../commands/UnarchiveHabitGroupsCommand.kt | 19 +++ .../org/isoron/uhabits/core/models/Habit.kt | 8 +- .../isoron/uhabits/core/models/HabitGroup.kt | 21 +-- .../uhabits/core/models/HabitGroupList.kt | 99 ++++++----- 11 files changed, 297 insertions(+), 73 deletions(-) create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardViewHolder.kt create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ArchiveHabitGroupsCommand.kt create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ChangeHabitGroupColorCommand.kt create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitGroupsCommand.kt create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/UnarchiveHabitGroupsCommand.kt diff --git a/uhabits-android/src/main/AndroidManifest.xml b/uhabits-android/src/main/AndroidManifest.xml index 8c7758439..56529c4bd 100644 --- a/uhabits-android/src/main/AndroidManifest.xml +++ b/uhabits-android/src/main/AndroidManifest.xml @@ -42,6 +42,14 @@ android:value=".activities.habits.list.ListHabitsActivity" /> + + + + diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt index 06412103b..b9c6ffaa4 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt @@ -38,7 +38,7 @@ import javax.inject.Inject * Provides data that backs a [HabitCardListView]. * * - * The data if fetched and cached by a [HabitCardListCache]. This adapter + * The data is fetched and cached by a [HabitCardListCache]. This adapter * also holds a list of items that have been selected. */ @ActivityScope diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt new file mode 100644 index 000000000..12832b4e4 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt @@ -0,0 +1,160 @@ +package org.isoron.uhabits.activities.habits.list.views + +import android.content.Context +import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.os.Handler +import android.os.Looper +import android.text.TextUtils +import android.view.Gravity +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.TextView +import org.isoron.platform.gui.toInt +import org.isoron.uhabits.R +import org.isoron.uhabits.activities.common.views.RingView +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.ModelObservable +import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior +import org.isoron.uhabits.inject.ActivityContext +import org.isoron.uhabits.utils.currentTheme +import org.isoron.uhabits.utils.dp +import org.isoron.uhabits.utils.sres +import javax.inject.Inject + +class HabitGroupCardViewFactory +@Inject constructor( + @ActivityContext val context: Context, + private val behavior: ListHabitsBehavior +) { + fun create() = HabitGroupCardView(context, behavior) +} + +class HabitGroupCardView( + @ActivityContext context: Context, + private val behavior: ListHabitsBehavior +) : FrameLayout(context), + ModelObservable.Listener { + + var habitGroup: HabitGroup? = null + set(newHabitGroup) { + if (isAttachedToWindow) { + field?.observable?.removeListener(this) + newHabitGroup?.observable?.addListener(this) + } + field = newHabitGroup + if (newHabitGroup != null) copyAttributesFrom(newHabitGroup) + } + + var score + get() = scoreRing.getPercentage().toDouble() + set(value) { + scoreRing.setPercentage(value.toFloat()) + scoreRing.setPrecision(1.0f / 16) + } + + private var innerFrame: LinearLayout + private var label: TextView + private var scoreRing: RingView + + private var currentToggleTaskId = 0 + + init { + scoreRing = RingView(context).apply { + val thickness = dp(3f) + val margin = dp(8f).toInt() + val ringSize = dp(15f).toInt() + layoutParams = LinearLayout.LayoutParams(ringSize, ringSize).apply { + setMargins(margin, 0, margin, 0) + gravity = Gravity.CENTER + } + setThickness(thickness) + } + + label = TextView(context).apply { + maxLines = 2 + ellipsize = TextUtils.TruncateAt.END + layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f) + if (SDK_INT >= Build.VERSION_CODES.Q) { + breakStrategy = BREAK_STRATEGY_BALANCED + } + } + + innerFrame = LinearLayout(context).apply { + gravity = Gravity.CENTER_VERTICAL + orientation = LinearLayout.HORIZONTAL + layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) + elevation = dp(1f) + + addView(scoreRing) + addView(label) + + setOnTouchListener { v, event -> + v.background.setHotspot(event.x, event.y) + false + } + } + + clipToPadding = false + layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT) + val margin = dp(3f).toInt() + setPadding(margin, 0, margin, margin) + addView(innerFrame) + } + + override fun onModelChange() { + Handler(Looper.getMainLooper()).post { + habitGroup?.let { copyAttributesFrom(it) } + } + } + + override fun setSelected(isSelected: Boolean) { + super.setSelected(isSelected) + updateBackground(isSelected) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + habitGroup?.observable?.addListener(this) + } + + override fun onDetachedFromWindow() { + habitGroup?.observable?.removeListener(this) + super.onDetachedFromWindow() + } + + private fun copyAttributesFrom(hgr: HabitGroup) { + fun getActiveColor(hgr: HabitGroup): Int { + return when (hgr.isArchived) { + true -> sres.getColor(R.attr.contrast60) + false -> currentTheme().color(hgr.color).toInt() + } + } + + val c = getActiveColor(hgr) + label.apply { + text = hgr.name + setTextColor(c) + } + scoreRing.apply { + setColor(c) + } + } + + private fun updateBackground(isSelected: Boolean) { + val background = when (isSelected) { + true -> R.drawable.selected_box + false -> R.drawable.ripple + } + innerFrame.setBackgroundResource(background) + } + + companion object { + fun (() -> Unit).delay(delayInMillis: Long) { + Handler(Looper.getMainLooper()).postDelayed(this, delayInMillis) + } + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardViewHolder.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardViewHolder.kt new file mode 100644 index 000000000..740d85009 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardViewHolder.kt @@ -0,0 +1,5 @@ +package org.isoron.uhabits.activities.habits.list.views + +import androidx.recyclerview.widget.RecyclerView + +class HabitGroupCardViewHolder(itemView: HabitGroupCardView) : RecyclerView.ViewHolder(itemView) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ArchiveHabitGroupsCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ArchiveHabitGroupsCommand.kt new file mode 100644 index 000000000..61705f31e --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ArchiveHabitGroupsCommand.kt @@ -0,0 +1,19 @@ +package org.isoron.uhabits.core.commands + +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.HabitGroupList + +data class ArchiveHabitGroupsCommand( + val habitGroupList: HabitGroupList, + val selected: List +) : Command { + override fun run() { + for (hgr in selected) { + hgr.isArchived = true + for (h in hgr.habitList) { + h.isArchived = true + } + } + habitGroupList.update(selected) + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ChangeHabitGroupColorCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ChangeHabitGroupColorCommand.kt new file mode 100644 index 000000000..a7655507f --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ChangeHabitGroupColorCommand.kt @@ -0,0 +1,16 @@ +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.PaletteColor + +data class ChangeHabitGroupColorCommand( + val habitGroupList: HabitGroupList, + val selected: List, + val newColor: PaletteColor +) : Command { + override fun run() { + for (hgr in selected) hgr.color = newColor + habitGroupList.update(selected) + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitGroupsCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitGroupsCommand.kt new file mode 100644 index 000000000..7e37b39c5 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitGroupsCommand.kt @@ -0,0 +1,13 @@ +package org.isoron.uhabits.core.commands + +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.HabitGroupList + +data class DeleteHabitGroupsCommand( + val habitGroupList: HabitGroupList, + val selected: List +) : Command { + override fun run() { + for (hgr in selected) habitGroupList.remove(hgr) + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/UnarchiveHabitGroupsCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/UnarchiveHabitGroupsCommand.kt new file mode 100644 index 000000000..166edd339 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/UnarchiveHabitGroupsCommand.kt @@ -0,0 +1,19 @@ +package org.isoron.uhabits.core.commands + +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.HabitGroupList + +data class UnarchiveHabitGroupsCommand( + val habitGroupList: HabitGroupList, + val selected: List +) : Command { + override fun run() { + for (hgr in selected) { + hgr.isArchived = false + for (h in hgr.habitList) { + h.isArchived = false + } + } + habitGroupList.update(selected) + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index ff57e823e..776b02be7 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -111,12 +111,8 @@ data class Habit( return computedEntries.getKnown().lastOrNull()?.timestamp ?: DateUtils.getTodayWithOffset() } - fun hierarchyLevel(): Int { - return if (parentID == null) { - 0 - } else { - 1 + parent!!.hierarchyLevel() - } + fun isInGroup(): Boolean { + return (parentID != null) } fun copyFrom(other: Habit) { 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 4526e1872..1bf42a89e 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 @@ -15,15 +15,12 @@ data class HabitGroup( var uuid: String? = null, var habitList: HabitList, val scores: ScoreList, - val streaks: StreakList, - var parentID: Long? = null, - var parentUUID: String? = null + val streaks: StreakList ) { init { if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "") } - var parent: HabitGroup? = null var observable = ModelObservable() val uriString: String @@ -80,8 +77,6 @@ data class HabitGroup( this.question = other.question this.reminder = other.reminder this.uuid = other.uuid - this.parentID = other.parentID - this.parentUUID = other.parentUUID } override fun equals(other: Any?): Boolean { @@ -97,8 +92,6 @@ data class HabitGroup( if (question != other.question) return false if (reminder != other.reminder) return false if (uuid != other.uuid) return false - if (parentID != other.parentID) return false - if (parentUUID != other.parentUUID) return false return true } @@ -113,19 +106,9 @@ data class HabitGroup( result = 31 * result + question.hashCode() result = 31 * result + (reminder?.hashCode() ?: 0) result = 31 * result + (uuid?.hashCode() ?: 0) - result = 31 * result + (parentID?.hashCode() ?: 0) - result = 31 * result + (parentUUID?.hashCode() ?: 0) return result } - fun hierarchyLevel(): Int { - return if (parentID == null) { - 0 - } else { - 1 + parent!!.hierarchyLevel() - } - } - - fun getHabitByUUIDDeep(uuid: String?): Habit? = + fun getHabitByUUID(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 f597eb1f7..471e0b62f 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 @@ -18,10 +18,10 @@ abstract class HabitGroupList : Iterable { protected val filter: HabitMatcher /** - * Creates a new HabitList. + * Creates a new HabitGroupList. * * Depending on the implementation, this list can either be empty or be - * populated by some pre-existing habits, for example, from a certain + * populated by some pre-existing habitgroups, for example, from a certain * database. */ constructor() { @@ -35,12 +35,12 @@ abstract class HabitGroupList : Iterable { } /** - * Inserts a new habit in the list. + * Inserts a new habit group in the list. * - * If the id of the habit is null, the list will assign it a new id, which + * If the id of the habit group is null, the list will assign it a new id, which * is guaranteed to be unique in the scope of the list. If id is not null, * the caller should make sure that the list does not already contain - * another habit with same id, otherwise a RuntimeException will be thrown. + * another habit group with same id, otherwise a RuntimeException will be thrown. * * @param habitGroup the habit to be inserted * @throws IllegalArgumentException if the habit is already on the list. @@ -49,28 +49,28 @@ abstract class HabitGroupList : Iterable { abstract fun add(habitGroup: HabitGroup) /** - * Returns the habit with specified id. + * Returns the habit group with specified id. * - * @param id the id of the habit - * @return the habit, or null if none exist + * @param id the id of the habit group + * @return the habit group, or null if none exist */ abstract fun getById(id: Long): HabitGroup? /** - * Returns the habit with specified UUID. + * Returns the habit group with specified UUID. * - * @param uuid the UUID of the habit - * @return the habit, or null if none exist + * @param uuid the UUID of the habit group + * @return the habit group, or null if none exist */ abstract fun getByUUID(uuid: String?): HabitGroup? /** * Returns the habit with the specified UUID which is - * present at any hierarchy within this list. + * present in any of the habit groups within this habit group list. */ - fun getHabitByUUIDDeep(uuid: String?): Habit? { + fun getHabitByUUID(uuid: String?): Habit? { for (hgr in this) { - val habit = hgr.getHabitByUUIDDeep(uuid) + val habit = hgr.getHabitByUUID(uuid) if (habit != null) { return habit } @@ -79,46 +79,46 @@ abstract class HabitGroupList : Iterable { } /** - * Returns the habit that occupies a certain position. + * Returns the habit group that occupies a certain position. * - * @param position the position of the desired habit - * @return the habit at that position + * @param position the position of the desired habit group + * @return the habit group at that position * @throws IndexOutOfBoundsException when the position is invalid */ abstract fun getByPosition(position: Int): HabitGroup /** - * Returns the list of habits that match a given condition. + * Returns the list of habit groups that match a given condition. * * @param matcher the matcher that checks the condition - * @return the list of matching habits + * @return the list of matching habit groups */ abstract fun getFiltered(matcher: HabitMatcher?): HabitGroupList abstract var primaryOrder: Order abstract var secondaryOrder: Order /** - * Returns the index of the given habit in the list, or -1 if the list does - * not contain the habit. + * Returns the index of the given habit group in the list, or -1 if the list does + * not contain the habit group. * - * @param h the habit - * @return the index of the habit, or -1 if not in the list + * @param h the habit group + * @return the index of the habit group, or -1 if not in the list */ abstract fun indexOf(h: HabitGroup): Int val isEmpty: Boolean get() = size() == 0 /** - * Removes the given habit from the list. + * Removes the given habit group from the list. * - * If the given habit is not in the list, does nothing. + * If the given habit group is not in the list, does nothing. * - * @param h the habit to be removed. + * @param h the habit group to be removed. */ abstract fun remove(h: HabitGroup) /** - * Removes all the habits from the list. + * Removes all the habit groups from the list. */ open fun removeAll() { val copy: MutableList = LinkedList() @@ -128,43 +128,49 @@ abstract class HabitGroupList : Iterable { } /** - * Changes the position of a habit in the list. + * Changes the position of a habit group in the list. * - * @param from the habit that should be moved - * @param to the habit that currently occupies the desired position + * @param from the habit group that should be moved + * @param to the habit group that currently occupies the desired position */ abstract fun reorder(from: HabitGroup, to: HabitGroup) open fun repair() {} /** - * Returns the number of habits in this list. + * Returns the number of habit groups in this list. * - * @return number of habits + * @return number of habit groups */ abstract fun size(): Int /** - * Notifies the list that a certain list of habits has been modified. + * Notifies the list that a certain list of habit groups has been modified. * * Depending on the implementation, this operation might trigger a write to - * disk, or do nothing at all. To make sure that the habits get persisted, + * disk, or do nothing at all. To make sure that the habit groups get persisted, * this operation must be called. * - * @param habitGroups the list of habits that have been modified. + * @param habitGroups the list of habit groups that have been modified. */ abstract fun update(habitGroups: List) /** - * Notifies the list that a certain habit has been modified. + * Notifies the list that a certain habit group has been modified. * * See [.update] for more details. * - * @param habitGroup the habit that has been modified. + * @param habitGroup the habit groups that has been modified. */ fun update(habitGroup: HabitGroup) { update(listOf(habitGroup)) } + /** + * For an empty Habit group list, and a given list of habits, + * populate the habit groups with their appropriate habits + * + * @param habitList list of habits to add to the groups + * */ fun populateGroupsWith(habitList: HabitList) { val toRemove = mutableListOf() for (habit in habitList) { @@ -187,10 +193,9 @@ abstract class HabitGroupList : Iterable { } /** - * Writes the list of habits to the given writer, in CSV format. There is - * one line for each habit, containing the fields name, description, - * frequency numerator, frequency denominator and color. The color is - * written in HTML format (#000000). + * Writes the list of habit groups to the given writer, in CSV format. There is + * one line for each habit group, containing the fields name, description, + * , and color. The color is written in HTML format (#000000). * * @param out the writer that will receive the result * @throws IOException if write operations fail @@ -208,13 +213,13 @@ abstract class HabitGroupList : Iterable { ) val csv = CSVWriter(out) csv.writeNext(header, false) - for (habit in this) { + for (hgr in this) { val cols = arrayOf( - String.format("%03d", indexOf(habit) + 1), - habit.name, - habit.question, - habit.description, - habit.color.toCsvColor() + String.format("%03d", indexOf(hgr) + 1), + hgr.name, + hgr.question, + hgr.description, + hgr.color.toCsvColor() ) csv.writeNext(cols, false) } From 506086f003d63295a2479df87dfd2771f0a5eb2a Mon Sep 17 00:00:00 2001 From: Dharanish Date: Mon, 1 Jul 2024 23:33:38 +0200 Subject: [PATCH 13/51] Can show habit group without interaction / scrolling --- .../habits/list/views/HabitCardViewTest.kt | 2 +- .../habits/list/ListHabitsRootView.kt | 2 +- .../habits/list/ListHabitsSelectionMenu.kt | 4 +- .../habits/list/views/AddButtonView.kt | 72 ++++ .../habits/list/views/HabitCardListAdapter.kt | 111 ++++-- .../habits/list/views/HabitCardListView.kt | 29 +- .../habits/list/views/HabitCardView.kt | 30 +- .../habits/list/views/HabitCardViewHolder.kt | 2 +- .../habits/list/views/HabitGroupCardView.kt | 13 +- .../list/views/HabitGroupCardViewHolder.kt | 5 - .../src/main/res/values/fontawesome.xml | 1 + .../isoron/uhabits/core/models/ScoreList.kt | 2 +- .../screens/habits/list/HabitCardListCache.kt | 333 +++++++++++++----- .../list/ListHabitsSelectionMenuBehavior.kt | 3 + 14 files changed, 477 insertions(+), 132 deletions(-) create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/AddButtonView.kt delete mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardViewHolder.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt index 20474090a..ad237f630 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt @@ -53,7 +53,7 @@ class HabitCardViewTest : BaseViewTest() { .getByInterval(today.minus(300), today) .map { it.value }.toIntArray() - view = component.getHabitCardViewFactory().create().apply { + view = component.getHabitCardViewFactory().createHabitCard().apply { habit = habit1 values = entries score = habit1.scores[today].value diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt index f0a542a0d..35de927d4 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt @@ -148,7 +148,7 @@ class ListHabitsRootView @Inject constructor( private fun updateEmptyView() { if (listAdapter.itemCount == 0) { - if (listAdapter.hasNoHabit()) { + if (listAdapter.hasNoHabit() && listAdapter.hasNoHabitGroup()) { llEmpty.showEmpty() } else { llEmpty.showDone() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt index ce5bc45a0..03e50fa6d 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt @@ -82,7 +82,7 @@ class ListHabitsSelectionMenu @Inject constructor( itemArchive.isVisible = behavior.canArchive() itemUnarchive.isVisible = behavior.canUnarchive() itemNotify.isVisible = prefs.isDeveloper - activeActionMode?.title = listAdapter.selected.size.toString() + activeActionMode?.title = listAdapter.selectedHabits.size.toString() return true } override fun onDestroyActionMode(mode: ActionMode?) { @@ -117,7 +117,7 @@ class ListHabitsSelectionMenu @Inject constructor( } R.id.action_notify -> { - for (h in listAdapter.selected) + for (h in listAdapter.selectedHabits) notificationTray.show(h, DateUtils.getToday(), 0) return true } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/AddButtonView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/AddButtonView.kt new file mode 100644 index 000000000..04477f889 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/AddButtonView.kt @@ -0,0 +1,72 @@ +package org.isoron.uhabits.activities.habits.list.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.text.TextPaint +import android.view.View +import android.view.View.MeasureSpec.EXACTLY +import org.isoron.uhabits.R +import org.isoron.uhabits.utils.getFontAwesome +import org.isoron.uhabits.utils.sp +import org.isoron.uhabits.utils.sres +import org.isoron.uhabits.utils.toMeasureSpec + +class AddButtonView( + context: Context +) : View(context), + View.OnClickListener { + + var onEdit: () -> Unit = { } + + private var drawer = Drawer() + + init { + setOnClickListener(this) + } + + override fun onClick(v: View) { + onEdit() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + drawer.draw(canvas) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val height = resources.getDimensionPixelSize(R.dimen.checkmarkHeight) + val width = resources.getDimensionPixelSize(R.dimen.checkmarkWidth) + super.onMeasure( + width.toMeasureSpec(EXACTLY), + height.toMeasureSpec(EXACTLY) + ) + } + + private inner class Drawer { + private val rect = RectF() + private val highContrastColor = sres.getColor(R.attr.contrast100) + + private val paint = TextPaint().apply { + typeface = getFontAwesome() + isAntiAlias = true + textAlign = Paint.Align.CENTER + } + + fun draw(canvas: Canvas) { + paint.color = highContrastColor + val id = R.string.fa_plus + paint.textSize = sp(12.0f) + paint.strokeWidth = 0f + paint.style = Paint.Style.FILL + + val label = resources.getString(id) + val em = paint.measureText("m") + + rect.set(0f, 0f, width.toFloat(), height.toFloat()) + rect.offset(0f, 0.4f * em) + canvas.drawText(label, rect.centerX(), rect.centerY(), paint) + } + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt index b9c6ffaa4..6f266a207 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt @@ -19,9 +19,10 @@ package org.isoron.uhabits.activities.habits.list.views import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Adapter import org.isoron.uhabits.activities.habits.list.MAX_CHECKMARK_COUNT import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitMatcher import org.isoron.uhabits.core.models.ModelObservable @@ -32,6 +33,7 @@ import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBeh import org.isoron.uhabits.core.utils.MidnightTimer import org.isoron.uhabits.inject.ActivityScope import java.util.LinkedList +import java.util.UUID import javax.inject.Inject /** @@ -46,14 +48,16 @@ class HabitCardListAdapter @Inject constructor( private val cache: HabitCardListCache, private val preferences: Preferences, private val midnightTimer: MidnightTimer -) : RecyclerView.Adapter(), +) : Adapter(), HabitCardListCache.Listener, MidnightTimer.MidnightListener, ListHabitsMenuBehavior.Adapter, ListHabitsSelectionMenuBehavior.Adapter { val observable: ModelObservable = ModelObservable() private var listView: HabitCardListView? = null - val selected: LinkedList = LinkedList() + val selectedHabits: LinkedList = LinkedList() + val selectedHabitGroups: LinkedList = LinkedList() + override fun atMidnight() { cache.refreshAllHabits() } @@ -66,17 +70,25 @@ class HabitCardListAdapter @Inject constructor( return cache.hasNoHabit() } + fun hasNoHabitGroup(): Boolean { + return cache.hasNoHabitGroup() + } + /** * Sets all items as not selected. */ override fun clearSelection() { - selected.clear() + selectedHabits.clear() notifyDataSetChanged() observable.notifyListeners() } override fun getSelected(): List { - return ArrayList(selected) + return ArrayList(selectedHabits) + } + + override fun getSelectedHabitGroups(): List { + return ArrayList(selectedHabitGroups) } /** @@ -91,11 +103,38 @@ class HabitCardListAdapter @Inject constructor( } override fun getItemCount(): Int { - return cache.habitCount + return cache.itemCount } override fun getItemId(position: Int): Long { - return getItem(position)!!.id!! + val uuidString = getItemUUID(position) + return if (uuidString != null) { + val formattedUUIDString = formatUUID(uuidString) + val uuid = UUID.fromString(formattedUUIDString) + uuid.mostSignificantBits and Long.MAX_VALUE + } else { + -1 + } + } + + fun getItemUUID(position: Int): String? { + val h = cache.getHabitByPosition(position) + val hgr = cache.getHabitGroupByPosition(position) + return if (h != null) { + h.uuid!! + } else if (hgr != null) { + hgr.uuid!! + } else { + null + } + } + + private fun formatUUID(uuidString: String): String { + return uuidString.substring(0, 8) + "-" + + uuidString.substring(8, 12) + "-" + + uuidString.substring(12, 16) + "-" + + uuidString.substring(16, 20) + "-" + + uuidString.substring(20, 32) } /** @@ -104,7 +143,7 @@ class HabitCardListAdapter @Inject constructor( * @return true if selection is empty, false otherwise */ val isSelectionEmpty: Boolean - get() = selected.isEmpty() + get() = selectedHabits.isEmpty() && selectedHabitGroups.isEmpty() val isSortable: Boolean get() = cache.primaryOrder == HabitList.Order.BY_POSITION @@ -122,11 +161,18 @@ class HabitCardListAdapter @Inject constructor( ) { if (listView == null) return val habit = cache.getHabitByPosition(position) - val score = cache.getScore(habit!!.id!!) - val checkmarks = cache.getCheckmarks(habit.id!!) - val notes = cache.getNotes(habit.id!!) - val selected = selected.contains(habit) - listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected) + if (habit != null) { + val score = cache.getScore(habit.uuid!!) + val checkmarks = cache.getCheckmarks(habit.uuid!!) + val notes = cache.getNotes(habit.uuid!!) + val selected = selectedHabits.contains(habit) + listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected) + } else { + val habitGroup = cache.getHabitGroupByPosition(position) + val score = cache.getScore(habitGroup!!.uuid!!) + val selected = selectedHabitGroups.contains(habitGroup) + listView!!.bindGroupCardView(holder, habitGroup, score, selected) + } } override fun onViewAttachedToWindow(holder: HabitCardViewHolder) { @@ -141,8 +187,22 @@ class HabitCardListAdapter @Inject constructor( parent: ViewGroup, viewType: Int ): HabitCardViewHolder { - val view = listView!!.createHabitCardView() - return HabitCardViewHolder(view) + if (viewType == 0) { + val view = listView!!.createHabitCardView() + return HabitCardViewHolder(view, null) + } else { + val view = listView!!.createHabitGroupCardView() + return HabitCardViewHolder(null, view) + } + } + + // function to override getItemViewType and return the type of the view. The view can either be a HabitCardView or a HabitGroupCardView + override fun getItemViewType(position: Int): Int { + return if (position < cache.habitCount) { + 0 + } else { + 1 + } } /** @@ -190,7 +250,11 @@ class HabitCardListAdapter @Inject constructor( * @param selected list of habits to be removed */ override fun performRemove(selected: List) { - for (habit in selected) cache.remove(habit.id!!) + for (habit in selected) cache.remove(habit.uuid!!) + } + + override fun performRemoveHabitGroup(selected: List) { + for (hgr in selected) cache.remove(hgr.uuid!!) } /** @@ -250,10 +314,17 @@ class HabitCardListAdapter @Inject constructor( * @param position position of the item to be toggled */ fun toggleSelection(position: Int) { - val h = getItem(position) ?: return - val k = selected.indexOf(h) - if (k < 0) selected.add(h) else selected.remove(h) - notifyDataSetChanged() + val h = cache.getHabitByPosition(position) + val hgr = cache.getHabitGroupByPosition(position) + if (h != null) { + val k = selectedHabits.indexOf(h) + if (k < 0) selectedHabits.add(h) else selectedHabits.remove(h) + notifyDataSetChanged() + } else if (hgr != null) { + val k = selectedHabitGroups.indexOf(hgr) + if (k < 0) selectedHabitGroups.add(hgr) else selectedHabitGroups.remove(hgr) + notifyDataSetChanged() + } } init { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt index fd6df425f..ceb20855c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt @@ -36,6 +36,7 @@ import dagger.Lazy import org.isoron.uhabits.R import org.isoron.uhabits.activities.common.views.BundleSavedState import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.inject.ActivityContext import javax.inject.Inject @@ -79,7 +80,11 @@ class HabitCardListView( } fun createHabitCardView(): HabitCardView { - return cardViewFactory.create() + return cardViewFactory.createHabitCard() + } + + fun createHabitGroupCardView(): HabitGroupCardView { + return cardViewFactory.createHabitGroupCard() } fun bindCardView( @@ -110,8 +115,28 @@ class HabitCardListView( return cardView } + fun bindGroupCardView( + holder: HabitCardViewHolder, + habitGroup: HabitGroup, + score: Double, + selected: Boolean + ): View { + val cardView = holder.itemView as HabitGroupCardView + cardView.habitGroup = habitGroup + cardView.isSelected = selected + cardView.score = score + + val detector = GestureDetector(context, CardViewGestureDetector(holder)) + cardView.setOnTouchListener { _, ev -> + detector.onTouchEvent(ev) + true + } + + return cardView + } + fun attachCardView(holder: HabitCardViewHolder) { - (holder.itemView as HabitCardView).dataOffset = dataOffset + (holder.itemView as? HabitCardView)?.dataOffset = dataOffset attachedHolders.add(holder) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index 84bd001fc..c2e0ad8f6 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -38,6 +38,7 @@ import org.isoron.platform.gui.toInt import org.isoron.uhabits.R import org.isoron.uhabits.activities.common.views.RingView import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.ModelObservable import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior @@ -55,7 +56,8 @@ class HabitCardViewFactory private val numberPanelFactory: NumberPanelViewFactory, private val behavior: ListHabitsBehavior ) { - fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior) + fun createHabitCard() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior) + fun createHabitGroupCard() = HabitGroupCardView(context, behavior) } class HabitCardView( @@ -285,6 +287,32 @@ class HabitCardView( } } + private fun copyAttributesFrom(hgr: HabitGroup) { + fun getActiveColor(habitGroup: HabitGroup): Int { + return when (habitGroup.isArchived) { + true -> sres.getColor(R.attr.contrast60) + false -> currentTheme().color(habitGroup.color).toInt() + } + } + + val c = getActiveColor(hgr) + label.apply { + text = hgr.name + setTextColor(c) + } + scoreRing.apply { + setColor(c) + } + checkmarkPanel.apply { + color = c + visibility = View.GONE + } + numberPanel.apply { + color = c + visibility = View.GONE + } + } + private fun triggerRipple(x: Float, y: Float) { val background = innerFrame.background background.setHotspot(x, y) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewHolder.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewHolder.kt index e01159360..84fc26a2b 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewHolder.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewHolder.kt @@ -21,4 +21,4 @@ package org.isoron.uhabits.activities.habits.list.views import androidx.recyclerview.widget.RecyclerView -class HabitCardViewHolder(itemView: HabitCardView) : RecyclerView.ViewHolder(itemView) +class HabitCardViewHolder(itemView1: HabitCardView?, itemView2: HabitGroupCardView?) : RecyclerView.ViewHolder(itemView1 ?: itemView2!!) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt index 12832b4e4..bc7495e40 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt @@ -23,15 +23,6 @@ import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.utils.currentTheme import org.isoron.uhabits.utils.dp import org.isoron.uhabits.utils.sres -import javax.inject.Inject - -class HabitGroupCardViewFactory -@Inject constructor( - @ActivityContext val context: Context, - private val behavior: ListHabitsBehavior -) { - fun create() = HabitGroupCardView(context, behavior) -} class HabitGroupCardView( @ActivityContext context: Context, @@ -56,6 +47,7 @@ class HabitGroupCardView( scoreRing.setPrecision(1.0f / 16) } + var addButtonView: AddButtonView private var innerFrame: LinearLayout private var label: TextView private var scoreRing: RingView @@ -83,6 +75,8 @@ class HabitGroupCardView( } } + addButtonView = AddButtonView(context) + innerFrame = LinearLayout(context).apply { gravity = Gravity.CENTER_VERTICAL orientation = LinearLayout.HORIZONTAL @@ -91,6 +85,7 @@ class HabitGroupCardView( addView(scoreRing) addView(label) + addView(addButtonView) setOnTouchListener { v, event -> v.background.setHotspot(event.x, event.y) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardViewHolder.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardViewHolder.kt deleted file mode 100644 index 740d85009..000000000 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardViewHolder.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.isoron.uhabits.activities.habits.list.views - -import androidx.recyclerview.widget.RecyclerView - -class HabitGroupCardViewHolder(itemView: HabitGroupCardView) : RecyclerView.ViewHolder(itemView) diff --git a/uhabits-android/src/main/res/values/fontawesome.xml b/uhabits-android/src/main/res/values/fontawesome.xml index 0ff190382..53e20cc46 100644 --- a/uhabits-android/src/main/res/values/fontawesome.xml +++ b/uhabits-android/src/main/res/values/fontawesome.xml @@ -24,6 +24,7 @@ + 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 bc6d139a1..89fff0dfc 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 @@ -146,7 +146,7 @@ class ScoreList { var current = to while (current >= from) { val habitScores = habitList.map { it.scores[current].value } - val averageScore = habitScores.average() + val averageScore = if (habitScores.isNotEmpty()) habitScores.average() else 0.0 map[current] = Score(current, averageScore) current = current.minus(1) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index c7d861813..9b33b64f9 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -25,15 +25,15 @@ import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.io.Logging import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.HabitGroupList import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitList.Order import org.isoron.uhabits.core.models.HabitMatcher import org.isoron.uhabits.core.tasks.Task import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset -import java.util.ArrayList import java.util.Arrays -import java.util.HashMap import java.util.LinkedList import java.util.TreeSet import javax.inject.Inject @@ -54,6 +54,7 @@ import javax.inject.Inject @AppScope class HabitCardListCache @Inject constructor( private val allHabits: HabitList, + private val allHabitGroups: HabitGroupList, private val commandRunner: CommandRunner, taskRunner: TaskRunner, logging: Logging @@ -66,6 +67,7 @@ class HabitCardListCache @Inject constructor( private var listener: Listener private val data: CacheData private var filteredHabits: HabitList + private var filteredHabitGroups: HabitGroupList private val taskRunner: TaskRunner @Synchronized @@ -74,13 +76,13 @@ class HabitCardListCache @Inject constructor( } @Synchronized - fun getCheckmarks(habitId: Long): IntArray { - return data.checkmarks[habitId]!! + fun getCheckmarks(habitUUID: String): IntArray { + return data.checkmarks[habitUUID]!! } @Synchronized - fun getNotes(habitId: Long): Array { - return data.notes[habitId]!! + fun getNotes(habitUUID: String): Array { + return data.notes[habitUUID]!! } @Synchronized @@ -88,21 +90,53 @@ class HabitCardListCache @Inject constructor( return allHabits.isEmpty } + @Synchronized + fun hasNoHabitGroup(): Boolean { + return allHabitGroups.isEmpty + } + /** * Returns the habits that occupies a certain position on the list. * - * @param position the position of the habit + * @param position the position of the list of habits and groups * @return the habit at given position or null if position is invalid */ @Synchronized fun getHabitByPosition(position: Int): Habit? { - return if (position < 0 || position >= data.habits.size) null else data.habits[position] + return if (position < 0 || position >= data.habits.size) { + null + } else { + data.habits[position] + } + } + + /** + * Returns the habit groups that occupies a certain position on the list. + * + * @param position the position of the list of habits and groups + * @return the habit group at given position or null if position is invalid + */ + @Synchronized + fun getHabitGroupByPosition(position: Int): HabitGroup? { + return if (position < data.habits.size || position >= data.habits.size + data.habitGroups.size) { + null + } else { + data.habitGroups[position - data.habits.size] + } } + @get:Synchronized + val itemCount: Int + get() = habitCount + habitGroupCount + @get:Synchronized val habitCount: Int get() = data.habits.size + @get:Synchronized + val habitGroupCount: Int + get() = data.habitGroups.size + @get:Synchronized @set:Synchronized var primaryOrder: Order @@ -110,6 +144,8 @@ class HabitCardListCache @Inject constructor( set(order) { allHabits.primaryOrder = order filteredHabits.primaryOrder = order + allHabitGroups.primaryOrder = order + filteredHabitGroups.primaryOrder = order refreshAllHabits() } @@ -120,12 +156,14 @@ class HabitCardListCache @Inject constructor( set(order) { allHabits.secondaryOrder = order filteredHabits.secondaryOrder = order + allHabitGroups.secondaryOrder = order + filteredHabitGroups.secondaryOrder = order refreshAllHabits() } @Synchronized - fun getScore(habitId: Long): Double { - return data.scores[habitId]!! + fun getScore(habitUUID: String): Double { + return data.scores[habitUUID]!! } @Synchronized @@ -137,7 +175,7 @@ class HabitCardListCache @Inject constructor( @Synchronized override fun onCommandFinished(command: Command) { if (command is CreateRepetitionCommand) { - command.habit.id?.let { refreshHabit(it) } + command.habit.uuid?.let { refreshHabit(it) } } else { refreshAllHabits() } @@ -157,27 +195,47 @@ class HabitCardListCache @Inject constructor( } @Synchronized - fun refreshHabit(id: Long) { - taskRunner.execute(RefreshTask(id)) + fun refreshHabit(uuid: String) { + taskRunner.execute(RefreshTask(uuid)) } @Synchronized - fun remove(id: Long) { - val h = data.idToHabit[id] ?: return - val position = data.habits.indexOf(h) - data.habits.removeAt(position) - data.idToHabit.remove(id) - data.checkmarks.remove(id) - data.notes.remove(id) - data.scores.remove(id) - listener.onItemRemoved(position) + fun remove(uuid: String) { + val h = data.uuidToHabit[uuid] + if (h != null) { + val position = data.habits.indexOf(h) + data.habits.removeAt(position) + data.uuidToHabit.remove(uuid) + data.checkmarks.remove(uuid) + data.notes.remove(uuid) + data.scores.remove(uuid) + listener.onItemRemoved(position) + } else { + val hgr = data.uuidToHabitGroup[uuid] + if (hgr != null) { + val position = data.habitGroups.indexOf(hgr) + data.habitGroups.removeAt(position) + data.uuidToHabitGroup.remove(uuid) + listener.onItemRemoved(position + data.habits.size) + } + } } @Synchronized fun reorder(from: Int, to: Int) { - val fromHabit = data.habits[from] - data.habits.removeAt(from) - data.habits.add(to, fromHabit) + if (data.habits.size in (from + 1)..to || data.habits.size in (to + 1)..from) { + logger.error("reorder: from and to are in different sections") + return + } + if (from < data.habits.size) { + val fromHabit = data.habits[from] + data.habits.removeAt(from) + data.habits.add(to, fromHabit) + } else { + val fromHabitGroup = data.habitGroups[from] + data.habitGroups.removeAt(from - data.habits.size) + data.habitGroups.add(to - data.habits.size, fromHabitGroup) + } listener.onItemMoved(from, to) } @@ -189,6 +247,7 @@ class HabitCardListCache @Inject constructor( @Synchronized fun setFilter(matcher: HabitMatcher) { filteredHabits = allHabits.getFiltered(matcher) + filteredHabitGroups = allHabitGroups.getFiltered(matcher) } @Synchronized @@ -209,21 +268,23 @@ class HabitCardListCache @Inject constructor( } private inner class CacheData { - val idToHabit: HashMap = HashMap() + val uuidToHabit: HashMap = HashMap() + val uuidToHabitGroup: HashMap = HashMap() val habits: MutableList - val checkmarks: HashMap - val scores: HashMap - val notes: HashMap> + val habitGroups: MutableList + val checkmarks: HashMap + val scores: HashMap + val notes: HashMap> @Synchronized fun copyCheckmarksFrom(oldData: CacheData) { val empty = IntArray(checkmarkCount) - for (id in idToHabit.keys) { - if (oldData.checkmarks.containsKey(id)) { - checkmarks[id] = - oldData.checkmarks[id]!! + for (uuid in uuidToHabit.keys) { + if (oldData.checkmarks.containsKey(uuid)) { + checkmarks[uuid] = + oldData.checkmarks[uuid]!! } else { - checkmarks[id] = empty + checkmarks[uuid] = empty } } } @@ -231,24 +292,32 @@ class HabitCardListCache @Inject constructor( @Synchronized fun copyNoteIndicatorsFrom(oldData: CacheData) { val empty = (0..checkmarkCount).map { "" }.toTypedArray() - for (id in idToHabit.keys) { - if (oldData.notes.containsKey(id)) { - notes[id] = - oldData.notes[id]!! + for (uuid in uuidToHabit.keys) { + if (oldData.notes.containsKey(uuid)) { + notes[uuid] = + oldData.notes[uuid]!! } else { - notes[id] = empty + notes[uuid] = empty } } } @Synchronized fun copyScoresFrom(oldData: CacheData) { - for (id in idToHabit.keys) { - if (oldData.scores.containsKey(id)) { - scores[id] = - oldData.scores[id]!! + for (uuid in uuidToHabit.keys) { + if (oldData.scores.containsKey(uuid)) { + scores[uuid] = + oldData.scores[uuid]!! } else { - scores[id] = 0.0 + scores[uuid] = 0.0 + } + } + for (uuid in uuidToHabitGroup.keys) { + if (oldData.scores.containsKey(uuid)) { + scores[uuid] = + oldData.scores[uuid]!! + } else { + scores[uuid] = 0.0 } } } @@ -256,9 +325,15 @@ class HabitCardListCache @Inject constructor( @Synchronized fun fetchHabits() { for (h in filteredHabits) { - if (h.id == null) continue + if (h.uuid == null) continue habits.add(h) - idToHabit[h.id] = h + uuidToHabit[h.uuid] = h + } + + for (hgr in filteredHabitGroups) { + if (hgr.uuid == null) continue + habitGroups.add(hgr) + uuidToHabitGroup[hgr.uuid] = hgr } } @@ -267,6 +342,7 @@ class HabitCardListCache @Inject constructor( */ init { habits = LinkedList() + habitGroups = LinkedList() checkmarks = HashMap() scores = HashMap() notes = HashMap() @@ -275,19 +351,19 @@ class HabitCardListCache @Inject constructor( private inner class RefreshTask : Task { private val newData: CacheData - private val targetId: Long? + private val targetUUID: String? private var isCancelled = false private var runner: TaskRunner? = null constructor() { newData = CacheData() - targetId = null + targetUUID = null isCancelled = false } - constructor(targetId: Long) { + constructor(targetUUID: String) { newData = CacheData() - this.targetId = targetId + this.targetUUID = targetUUID } @Synchronized @@ -307,8 +383,8 @@ class HabitCardListCache @Inject constructor( for (position in newData.habits.indices) { if (isCancelled) return val habit = newData.habits[position] - if (targetId != null && targetId != habit.id) continue - newData.scores[habit.id] = habit.scores[today].value + if (targetUUID != null && targetUUID != habit.uuid) continue + newData.scores[habit.uuid] = habit.scores[today].value val list: MutableList = ArrayList() val notes: MutableList = ArrayList() for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) { @@ -316,10 +392,18 @@ class HabitCardListCache @Inject constructor( notes.add(note) } val entries = list.toTypedArray() - newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries) - newData.notes[habit.id] = notes.toTypedArray() + newData.checkmarks[habit.uuid] = ArrayUtils.toPrimitive(entries) + newData.notes[habit.uuid] = notes.toTypedArray() runner!!.publishProgress(this, position) } + + for (position in newData.habitGroups.indices) { + if (isCancelled) return + val hgr = newData.habitGroups[position] + if (targetUUID != null && targetUUID != hgr.uuid) continue + newData.scores[hgr.uuid] = hgr.scores[today].value + runner!!.publishProgress(this, position + newData.habits.size) + } } @Synchronized @@ -340,15 +424,29 @@ class HabitCardListCache @Inject constructor( @Synchronized private fun performInsert(habit: Habit, position: Int) { - val id = habit.id + val uuid = habit.uuid data.habits.add(position, habit) - data.idToHabit[id] = habit - data.scores[id] = newData.scores[id]!! - data.checkmarks[id] = newData.checkmarks[id]!! - data.notes[id] = newData.notes[id]!! + data.uuidToHabit[uuid] = habit + data.scores[uuid] = newData.scores[uuid]!! + data.checkmarks[uuid] = newData.checkmarks[uuid]!! + data.notes[uuid] = newData.notes[uuid]!! listener.onItemInserted(position) } + @Synchronized + private fun performInsert(habitGroup: HabitGroup, position: Int) { + val newPosition = if (position < data.habits.size) { + data.habits.size + } else { + position + } + val uuid = habitGroup.uuid + data.habitGroups.add(newPosition - data.habits.size, habitGroup) + data.uuidToHabitGroup[uuid] = habitGroup + data.scores[uuid] = newData.scores[uuid]!! + listener.onItemInserted(newPosition) + } + @Synchronized private fun performMove( habit: Habit, @@ -359,7 +457,7 @@ class HabitCardListCache @Inject constructor( // Workaround for https://github.com/iSoron/uhabits/issues/968 val checkedToPosition = if (toPosition > data.habits.size) { - logger.error("performMove: $toPosition is strictly higher than ${data.habits.size}") + logger.error("performMove: $toPosition for habit is strictly higher than ${data.habits.size}") data.habits.size } else { toPosition @@ -369,57 +467,114 @@ class HabitCardListCache @Inject constructor( listener.onItemMoved(fromPosition, checkedToPosition) } + private fun performMove( + habitGroup: HabitGroup, + fromPosition: Int, + toPosition: Int + ) { + data.habitGroups.removeAt(fromPosition) + + // Workaround for https://github.com/iSoron/uhabits/issues/968 + val checkedToPosition = if (toPosition < data.habits.size) { + logger.error("performMove: $toPosition for habit group is strictly lower than ${data.habits.size}") + data.habits.size + } else if (toPosition > data.habits.size + data.habitGroups.size) { + logger.error("performMove: $toPosition for habit group is strictly higher than ${data.habits.size + data.habitGroups.size}") + data.habits.size + data.habitGroups.size + } else { + toPosition + } + + data.habitGroups.add(checkedToPosition - data.habits.size, habitGroup) + listener.onItemMoved(fromPosition, checkedToPosition) + } + @Synchronized - private fun performUpdate(id: Long, position: Int) { - val oldScore = data.scores[id]!! - val oldCheckmarks = data.checkmarks[id] - val oldNoteIndicators = data.notes[id] - val newScore = newData.scores[id]!! - val newCheckmarks = newData.checkmarks[id]!! - val newNoteIndicators = newData.notes[id]!! + private fun performUpdate(uuid: String, position: Int) { var unchanged = true + val oldScore = data.scores[uuid]!! + val newScore = newData.scores[uuid]!! if (oldScore != newScore) unchanged = false - if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false - if (!Arrays.equals(oldNoteIndicators, newNoteIndicators)) unchanged = false + + if (position < data.habits.size) { + val oldCheckmarks = data.checkmarks[uuid] + val newCheckmarks = newData.checkmarks[uuid]!! + val oldNoteIndicators = data.notes[uuid] + val newNoteIndicators = newData.notes[uuid]!! + if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false + if (!Arrays.equals(oldNoteIndicators, newNoteIndicators)) unchanged = false + if (unchanged) return + data.checkmarks[uuid] = newCheckmarks + data.notes[uuid] = newNoteIndicators + } + if (unchanged) return - data.scores[id] = newScore - data.checkmarks[id] = newCheckmarks - data.notes[id] = newNoteIndicators + data.scores[uuid] = newScore listener.onItemChanged(position) } @Synchronized private fun processPosition(currentPosition: Int) { - val habit = newData.habits[currentPosition] - val id = habit.id - val prevPosition = data.habits.indexOf(habit) - if (prevPosition < 0) { - performInsert(habit, currentPosition) + if (currentPosition < newData.habits.size) { + val habit = newData.habits[currentPosition] + val uuid = habit.uuid + val prevPosition = data.habits.indexOf(habit) + if (prevPosition < 0) { + performInsert(habit, currentPosition) + } else { + if (prevPosition != currentPosition) { + performMove( + habit, + prevPosition, + currentPosition + ) + } + if (uuid == null) throw NullPointerException() + performUpdate(uuid, currentPosition) + } } else { - if (prevPosition != currentPosition) { - performMove( - habit, - prevPosition, - currentPosition - ) + val habitGroup = newData.habitGroups[currentPosition - data.habits.size] + val uuid = habitGroup.uuid + val prevPosition = data.habitGroups.indexOf(habitGroup) + data.habits.size + if (prevPosition < 0) { + performInsert(habitGroup, currentPosition) + } else { + if (prevPosition != currentPosition) { + performMove( + habitGroup, + prevPosition, + currentPosition + ) + } + if (uuid == null) throw NullPointerException() + performUpdate(uuid, currentPosition) } - if (id == null) throw NullPointerException() - performUpdate(id, currentPosition) } } @Synchronized private fun processRemovedHabits() { - val before: Set = data.idToHabit.keys - val after: Set = newData.idToHabit.keys - val removed: MutableSet = TreeSet(before) + val before: Set = data.uuidToHabit.keys + val after: Set = newData.uuidToHabit.keys + val removed: MutableSet = TreeSet(before) + removed.removeAll(after) + for (uuid in removed) remove(uuid!!) + processRemovedHabitGroups() + } + + @Synchronized + private fun processRemovedHabitGroups() { + val before: Set = data.uuidToHabitGroup.keys + val after: Set = newData.uuidToHabitGroup.keys + val removed: MutableSet = TreeSet(before) removed.removeAll(after) - for (id in removed) remove(id!!) + for (uuid in removed) remove(uuid!!) } } init { filteredHabits = allHabits + filteredHabitGroups = allHabitGroups this.taskRunner = taskRunner listener = object : Listener {} data = CacheData() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt index 00d90a1a7..49df8afab 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt @@ -24,6 +24,7 @@ import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.DeleteHabitsCommand import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback @@ -88,7 +89,9 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( interface Adapter { fun clearSelection() fun getSelected(): List + fun getSelectedHabitGroups(): List fun performRemove(selected: List) + fun performRemoveHabitGroup(selected: List) } interface Screen { From 676f141d9913e8fb5f49a17ab09c4f7d06e532e1 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Mon, 1 Jul 2024 23:41:02 +0200 Subject: [PATCH 14/51] Implement scrolling with group --- .../activities/habits/list/views/HabitCardListView.kt | 9 +++++++-- .../activities/habits/list/views/HabitGroupCardView.kt | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt index ceb20855c..a656b4d48 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt @@ -63,8 +63,13 @@ class HabitCardListView( set(value) { field = value attachedHolders - .map { it.itemView as HabitCardView } - .forEach { it.dataOffset = value } + .forEach { + if (it.itemView is HabitCardView) { + (it.itemView as HabitCardView).dataOffset = value + } else { + (it.itemView as HabitGroupCardView).dataOffset = value + } + } } private val attachedHolders = mutableListOf() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt index bc7495e40..aa207807b 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt @@ -30,6 +30,8 @@ class HabitGroupCardView( ) : FrameLayout(context), ModelObservable.Listener { + var dataOffset = 0 + var habitGroup: HabitGroup? = null set(newHabitGroup) { if (isAttachedToWindow) { From 17e6c4f6e922fd7861b0c7aa39025caf2eb01967 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Tue, 2 Jul 2024 16:10:02 +0200 Subject: [PATCH 15/51] Fix habit group creation --- .../uhabits/activities/habits/list/views/HabitCardView.kt | 5 +++-- .../activities/habits/list/views/HabitGroupCardView.kt | 2 ++ .../jvmMain/java/org/isoron/uhabits/core/models/Habit.kt | 2 ++ .../core/ui/screens/habits/list/HabitCardListCache.kt | 6 +++++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index c2e0ad8f6..596b25668 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -136,10 +136,11 @@ class HabitCardView( init { scoreRing = RingView(context).apply { val thickness = dp(3f) - val margin = dp(8f).toInt() + val rightMargin = dp(8f).toInt() val ringSize = dp(15f).toInt() + val leftMargin = if (habit?.isSubHabit() == true) dp(30f).toInt() else dp(8f).toInt() layoutParams = LinearLayout.LayoutParams(ringSize, ringSize).apply { - setMargins(margin, 0, margin, 0) + setMargins(leftMargin, 0, rightMargin, 0) gravity = Gravity.CENTER } setThickness(thickness) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt index aa207807b..03f88d011 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt @@ -1,6 +1,7 @@ package org.isoron.uhabits.activities.habits.list.views import android.content.Context +import android.graphics.Typeface import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED import android.os.Build import android.os.Build.VERSION.SDK_INT @@ -75,6 +76,7 @@ class HabitGroupCardView( if (SDK_INT >= Build.VERSION_CODES.Q) { breakStrategy = BREAK_STRATEGY_BALANCED } + setTypeface(typeface, Typeface.BOLD) } addButtonView = AddButtonView(context) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index 776b02be7..85ee6402a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -56,6 +56,8 @@ data class Habit( val uriString: String get() = "content://org.isoron.uhabits/habit/$id" + fun isSubHabit(): Boolean = parentUUID != null + fun hasReminder(): Boolean = reminder != null fun isCompletedToday(): Boolean { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index 9b33b64f9..6e62116a4 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -472,6 +472,10 @@ class HabitCardListCache @Inject constructor( fromPosition: Int, toPosition: Int ) { + if (fromPosition < data.habits.size || fromPosition > data.habits.size + data.habitGroups.size) { + logger.error("performMove: $fromPosition for habit group is out of bounds") + return + } data.habitGroups.removeAt(fromPosition) // Workaround for https://github.com/iSoron/uhabits/issues/968 @@ -536,7 +540,7 @@ class HabitCardListCache @Inject constructor( val habitGroup = newData.habitGroups[currentPosition - data.habits.size] val uuid = habitGroup.uuid val prevPosition = data.habitGroups.indexOf(habitGroup) + data.habits.size - if (prevPosition < 0) { + if (prevPosition < data.habits.size) { performInsert(habitGroup, currentPosition) } else { if (prevPosition != currentPosition) { From cac62c027855daaa1876b358de6a5e8783e52bb9 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Wed, 3 Jul 2024 00:17:40 +0200 Subject: [PATCH 16/51] Add Habits to groups without displaying --- .../habits/edit/EditHabitActivity.kt | 22 ++++++++++++++++--- .../activities/habits/edit/HabitTypeDialog.kt | 10 ++++++--- .../habits/list/ListHabitsScreen.kt | 4 ++-- .../habits/list/views/AddButtonView.kt | 9 ++++---- .../habits/list/views/HabitGroupCardView.kt | 3 ++- .../isoron/uhabits/intents/IntentFactory.kt | 5 ++++- .../org/isoron/uhabits/core/models/Habit.kt | 1 + .../isoron/uhabits/core/models/HabitGroup.kt | 2 ++ .../isoron/uhabits/core/models/HabitList.kt | 2 ++ .../core/models/memory/MemoryHabitList.kt | 2 -- .../core/models/sqlite/SQLiteHabitList.kt | 3 ++- .../screens/habits/list/HabitCardListCache.kt | 2 +- .../habits/list/ListHabitsMenuBehavior.kt | 6 ++--- 13 files changed, 50 insertions(+), 21 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt index a2f329d71..d5cc898b1 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt @@ -47,6 +47,7 @@ import org.isoron.uhabits.core.commands.CreateHabitCommand import org.isoron.uhabits.core.commands.EditHabitCommand import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.HabitType import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.PaletteColor @@ -76,6 +77,7 @@ class EditHabitActivity : AppCompatActivity() { var habitId = -1L lateinit var habitType: HabitType + var parentGroup: HabitGroup? = null var unit = "" var color = PaletteColor(11) var androidColor = 0 @@ -110,6 +112,7 @@ class EditHabitActivity : AppCompatActivity() { reminderMin = it.minute reminderDays = it.days } + parentGroup = habit.parent binding.nameInput.setText(habit.name) binding.questionInput.setText(habit.question) binding.notesInput.setText(habit.description) @@ -117,6 +120,10 @@ class EditHabitActivity : AppCompatActivity() { binding.targetInput.setText(habit.targetValue.toString()) } else { habitType = HabitType.fromInt(intent.getIntExtra("habitType", HabitType.YES_NO.value)) + if (intent.hasExtra("parentGroupUUID")) { + val parentGroupUUID = intent.getStringExtra("parentGroupUUID")!! + parentGroup = component.habitGroupList.getByUUID(parentGroupUUID) + } } if (state != null) { @@ -260,9 +267,15 @@ class EditHabitActivity : AppCompatActivity() { val component = (application as HabitsApplication).component val habit = component.modelFactory.buildHabit() + val habitList = if (parentGroup != null) { + parentGroup!!.habitList + } else { + component.habitList + } + var original: Habit? = null if (habitId >= 0) { - original = component.habitList.getById(habitId)!! + original = habitList.getById(habitId)!! habit.copyFrom(original) } @@ -283,17 +296,20 @@ class EditHabitActivity : AppCompatActivity() { habit.unit = binding.unitInput.text.trim().toString() } habit.type = habitType + habit.parent = parentGroup + habit.parentID = parentGroup?.id + habit.parentUUID = parentGroup?.uuid val command = if (habitId >= 0) { EditHabitCommand( - component.habitList, + habitList, habitId, habit ) } else { CreateHabitCommand( component.modelFactory, - component.habitList, + habitList, habit ) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/HabitTypeDialog.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/HabitTypeDialog.kt index d7acd8f03..299457a89 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/HabitTypeDialog.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/HabitTypeDialog.kt @@ -29,7 +29,7 @@ import org.isoron.uhabits.core.models.HabitType import org.isoron.uhabits.databinding.SelectHabitTypeBinding import org.isoron.uhabits.intents.IntentFactory -class HabitTypeDialog : AppCompatDialogFragment() { +class HabitTypeDialog(val parentUUID: String? = null) : AppCompatDialogFragment() { override fun getTheme() = R.style.Translucent override fun onCreateView( @@ -40,13 +40,13 @@ class HabitTypeDialog : AppCompatDialogFragment() { val binding = SelectHabitTypeBinding.inflate(inflater, container, false) binding.buttonYesNo.setOnClickListener { - val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.YES_NO.value) + val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.YES_NO.value, parentUUID) startActivity(intent) dismiss() } binding.buttonMeasurable.setOnClickListener { - val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.NUMERICAL.value) + val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.NUMERICAL.value, parentUUID) startActivity(intent) dismiss() } @@ -57,6 +57,10 @@ class HabitTypeDialog : AppCompatDialogFragment() { dismiss() } + if (parentUUID != null) { + binding.buttonHabitGroup.visibility = View.GONE + } + binding.background.setOnClickListener { dismiss() } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt index e1f0418a3..c6ad6fae8 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt @@ -164,8 +164,8 @@ class ListHabitsScreen activity.startActivity(intent) } - override fun showSelectHabitTypeDialog() { - val dialog = HabitTypeDialog() + override fun showSelectHabitTypeDialog(parentUUID: String?) { + val dialog = HabitTypeDialog(parentUUID) dialog.show(activity.supportFragmentManager, "habitType") } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/AddButtonView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/AddButtonView.kt index 04477f889..3cff41fcc 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/AddButtonView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/AddButtonView.kt @@ -8,18 +8,19 @@ import android.text.TextPaint import android.view.View import android.view.View.MeasureSpec.EXACTLY import org.isoron.uhabits.R +import org.isoron.uhabits.activities.habits.list.ListHabitsActivity +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.sp import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.toMeasureSpec class AddButtonView( - context: Context + context: Context, + var habitGroup: HabitGroup? ) : View(context), View.OnClickListener { - var onEdit: () -> Unit = { } - private var drawer = Drawer() init { @@ -27,7 +28,7 @@ class AddButtonView( } override fun onClick(v: View) { - onEdit() + (context as ListHabitsActivity).component.listHabitsMenu.behavior.onCreateHabit(habitGroup!!.uuid) } override fun onDraw(canvas: Canvas) { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt index 03f88d011..4bbffa7e2 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt @@ -41,6 +41,7 @@ class HabitGroupCardView( } field = newHabitGroup if (newHabitGroup != null) copyAttributesFrom(newHabitGroup) + addButtonView.habitGroup = newHabitGroup } var score @@ -79,7 +80,7 @@ class HabitGroupCardView( setTypeface(typeface, Typeface.BOLD) } - addButtonView = AddButtonView(context) + addButtonView = AddButtonView(context, habitGroup) innerFrame = LinearLayout(context).apply { gravity = Gravity.CENTER_VERTICAL 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 33fa31aa3..b2beb3d46 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 @@ -97,9 +97,12 @@ class IntentFactory return intent } - fun startEditActivity(context: Context, habitType: Int): Intent { + fun startEditActivity(context: Context, habitType: Int, parentUUID: String?): Intent { val intent = startEditActivity(context) intent.putExtra("habitType", habitType) + if (parentUUID != null) { + intent.putExtra("parentGroupUUID", parentUUID) + } return intent } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index 85ee6402a..2d41d0384 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -132,6 +132,7 @@ data class Habit( this.type = other.type this.unit = other.unit this.uuid = other.uuid + this.parent = other.parent this.parentID = other.parentID this.parentUUID = other.parentUUID } 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 1bf42a89e..0e3293493 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 @@ -19,6 +19,7 @@ data class HabitGroup( ) { init { if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "") + habitList.groupUUID = this.uuid } var observable = ModelObservable() @@ -77,6 +78,7 @@ data class HabitGroup( this.question = other.question this.reminder = other.reminder this.uuid = other.uuid + this.habitList.groupUUID = this.uuid } override fun equals(other: Any?): Boolean { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt index 74ee0a700..61db7ed76 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt @@ -34,6 +34,8 @@ abstract class HabitList : Iterable { @JvmField protected val filter: HabitMatcher + var groupUUID: String? = null + /** * Creates a new HabitList. * diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt index 6e6bace1a..e0b9037a6 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt @@ -22,8 +22,6 @@ import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitMatcher import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset -import java.util.ArrayList -import java.util.Comparator import java.util.LinkedList import java.util.Objects diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt index 87d05b9ca..f7ef12a96 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt @@ -37,6 +37,7 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory private fun loadRecords() { if (loaded) return loaded = true + list.groupUUID = this.groupUUID list.removeAll() val records = repository.findAll("order by position") var shouldRebuildOrder = false @@ -45,7 +46,7 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory val h = modelFactory.buildHabit() rec.copyTo(h) (h.originalEntries as SQLiteEntryList).habitId = h.id - list.add(h) + if (h.parentUUID == list.groupUUID) list.add(h) } if (shouldRebuildOrder) rebuildOrder() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index 6e62116a4..c18ea5320 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -476,7 +476,7 @@ class HabitCardListCache @Inject constructor( logger.error("performMove: $fromPosition for habit group is out of bounds") return } - data.habitGroups.removeAt(fromPosition) + data.habitGroups.removeAt(fromPosition - data.habits.size) // Workaround for https://github.com/iSoron/uhabits/issues/968 val checkedToPosition = if (toPosition < data.habits.size) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt index 7769dd32b..b3b08751b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsMenuBehavior.kt @@ -33,8 +33,8 @@ class ListHabitsMenuBehavior @Inject constructor( private var showCompleted: Boolean private var showArchived: Boolean - fun onCreateHabit() { - screen.showSelectHabitTypeDialog() + fun onCreateHabit(parentUUID: String? = null) { + screen.showSelectHabitTypeDialog(parentUUID) } fun onViewFAQ() { @@ -132,7 +132,7 @@ class ListHabitsMenuBehavior @Inject constructor( fun showAboutScreen() fun showFAQScreen() fun showSettingsScreen() - fun showSelectHabitTypeDialog() + fun showSelectHabitTypeDialog(parentUUID: String? = null) } init { From a2cf78f82390f1a76b9dde8b6c26f21ce5858bb0 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Wed, 3 Jul 2024 09:14:16 +0200 Subject: [PATCH 17/51] Add show habit group activity --- uhabits-android/src/main/AndroidManifest.xml | 8 + .../uhabits/activities/HabitsDirFinder.kt | 11 ++ .../habits/list/ListHabitsScreen.kt | 6 + .../habits/list/views/HabitCardListAdapter.kt | 8 + .../list/views/HabitCardListController.kt | 11 +- .../habits/show/ShowHabitGroupActivity.kt | 152 ++++++++++++++++++ .../habits/show/ShowHabitGroupMenu.kt | 32 ++++ .../habits/show/ShowHabitGroupView.kt | 35 ++++ .../isoron/uhabits/intents/IntentFactory.kt | 6 + .../src/main/res/menu/show_habit_group.xml | 35 ++++ .../isoron/uhabits/core/models/StreakList.kt | 4 +- .../screens/habits/list/ListHabitsBehavior.kt | 6 + .../ui/screens/habits/show/ShowHabitGroup.kt | 78 +++++++++ .../show/ShowHabitGroupMenuPresenter.kt | 46 ++++++ .../ui/screens/habits/show/views/NotesCard.kt | 5 + .../screens/habits/show/views/OverviewCard.kt | 23 +++ .../ui/screens/habits/show/views/ScoreCard.kt | 40 +++++ .../screens/habits/show/views/StreakCart.kt | 9 ++ .../screens/habits/show/views/SubtitleCard.kt | 13 ++ 19 files changed, 524 insertions(+), 4 deletions(-) create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupActivity.kt create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupMenu.kt create mode 100644 uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupView.kt create mode 100644 uhabits-android/src/main/res/menu/show_habit_group.xml create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroup.kt create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroupMenuPresenter.kt diff --git a/uhabits-android/src/main/AndroidManifest.xml b/uhabits-android/src/main/AndroidManifest.xml index 56529c4bd..36f3928c0 100644 --- a/uhabits-android/src/main/AndroidManifest.xml +++ b/uhabits-android/src/main/AndroidManifest.xml @@ -80,6 +80,14 @@ android:value=".activities.habits.list.ListHabitsActivity" /> + + + + diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/HabitsDirFinder.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/HabitsDirFinder.kt index 953ee554c..4692799c4 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/HabitsDirFinder.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/HabitsDirFinder.kt @@ -20,6 +20,7 @@ package org.isoron.uhabits.activities import org.isoron.uhabits.AndroidDirFinder import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior +import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitGroupMenuPresenter import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter import java.io.File import javax.inject.Inject @@ -33,3 +34,13 @@ constructor( return androidDirFinder.getFilesDir("CSV")!! } } + +class HabitGroupsDirFinder @Inject +constructor( + private val androidDirFinder: AndroidDirFinder +) : ShowHabitGroupMenuPresenter.System, ListHabitsBehavior.DirFinder { + + override fun getCSVOutputDir(): File { + return androidDirFinder.getFilesDir("CSV")!! + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt index c6ad6fae8..4441cdbdc 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt @@ -45,6 +45,7 @@ import org.isoron.uhabits.core.commands.DeleteHabitsCommand import org.isoron.uhabits.core.commands.EditHabitCommand import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.tasks.TaskRunner @@ -188,6 +189,11 @@ class ListHabitsScreen activity.startActivity(intent) } + override fun showHabitGroupScreen(hgr: HabitGroup) { + val intent = intentFactory.startShowHabitGroupActivity(activity, hgr) + activity.startActivity(intent) + } + fun showImportScreen() { val intent = intentFactory.openDocument() activity.startActivityForResult(intent, REQUEST_OPEN_DOCUMENT) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt index 6f266a207..264978c66 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt @@ -102,6 +102,14 @@ class HabitCardListAdapter @Inject constructor( return cache.getHabitByPosition(position) } + fun getHabit(position: Int): Habit? { + return cache.getHabitByPosition(position) + } + + fun getHabitGroup(position: Int): HabitGroup? { + return cache.getHabitGroupByPosition(position) + } + override fun getItemCount(): Int { return cache.itemCount } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListController.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListController.kt index a70805139..9ae4243c6 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListController.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListController.kt @@ -114,8 +114,15 @@ class HabitCardListController @Inject constructor( */ internal inner class NormalMode : Mode { override fun onItemClick(position: Int) { - val habit = adapter.getItem(position) ?: return - behavior.onClickHabit(habit) + val habit = adapter.getHabit(position) + if (habit != null) { + behavior.onClickHabit(habit) + } else { + val hgr = adapter.getHabitGroup(position) + if (hgr != null) { + behavior.onClickHabitGroup(hgr) + } + } } override fun onItemLongClick(position: Int): Boolean { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupActivity.kt new file mode 100644 index 000000000..97bab67c2 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupActivity.kt @@ -0,0 +1,152 @@ +package org.isoron.uhabits.activities.habits.show + +import android.content.ContentUris +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.isoron.uhabits.AndroidDirFinder +import org.isoron.uhabits.HabitsApplication +import org.isoron.uhabits.R +import org.isoron.uhabits.activities.AndroidThemeSwitcher +import org.isoron.uhabits.activities.HabitGroupsDirFinder +import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog +import org.isoron.uhabits.core.commands.Command +import org.isoron.uhabits.core.commands.CommandRunner +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback +import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitGroupMenuPresenter +import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitGroupPresenter +import org.isoron.uhabits.intents.IntentFactory +import org.isoron.uhabits.utils.dismissCurrentAndShow +import org.isoron.uhabits.utils.dismissCurrentDialog +import org.isoron.uhabits.utils.showMessage +import org.isoron.uhabits.utils.showSendFileScreen +import org.isoron.uhabits.widgets.WidgetUpdater + +class ShowHabitGroupActivity : AppCompatActivity(), CommandRunner.Listener { + + private lateinit var commandRunner: CommandRunner + private lateinit var menu: ShowHabitGroupMenu + private lateinit var view: ShowHabitGroupView + private lateinit var habitGroup: HabitGroup + private lateinit var preferences: Preferences + private lateinit var themeSwitcher: AndroidThemeSwitcher + private lateinit var widgetUpdater: WidgetUpdater + + private val scope = CoroutineScope(Dispatchers.Main) + private lateinit var presenter: ShowHabitGroupPresenter + private val screen = Screen() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val appComponent = (applicationContext as HabitsApplication).component + val habitGroupList = appComponent.habitGroupList + habitGroup = habitGroupList.getById(ContentUris.parseId(intent.data!!))!! + preferences = appComponent.preferences + commandRunner = appComponent.commandRunner + widgetUpdater = appComponent.widgetUpdater + + themeSwitcher = AndroidThemeSwitcher(this, preferences) + themeSwitcher.apply() + + presenter = ShowHabitGroupPresenter( + commandRunner = commandRunner, + habitGroup = habitGroup, + preferences = preferences, + screen = screen + ) + + view = ShowHabitGroupView(this) + + val menuPresenter = ShowHabitGroupMenuPresenter( + commandRunner = commandRunner, + habitGroup = habitGroup, + habitGroupList = habitGroupList, + screen = screen, + system = HabitGroupsDirFinder(AndroidDirFinder(this)), + taskRunner = appComponent.taskRunner + ) + + menu = ShowHabitGroupMenu( + activity = this, + presenter = menuPresenter, + preferences = preferences + ) + + view.setListener(presenter) + setContentView(view) + } + + override fun onCreateOptionsMenu(m: Menu): Boolean { + return menu.onCreateOptionsMenu(m) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return menu.onOptionsItemSelected(item) + } + + override fun onResume() { + super.onResume() + commandRunner.addListener(this) + screen.refresh() + } + + override fun onPause() { + dismissCurrentDialog() + commandRunner.removeListener(this) + super.onPause() + } + + override fun onCommandFinished(command: Command) { + screen.refresh() + } + + inner class Screen : ShowHabitGroupMenuPresenter.Screen, ShowHabitGroupPresenter.Screen { + override fun updateWidgets() { + widgetUpdater.updateWidgets() + } + + override fun refresh() { + scope.launch { + view.setState( + ShowHabitGroupPresenter.buildState( + habitGroup = habitGroup, + preferences = preferences, + theme = themeSwitcher.currentTheme + ) + ) + } + } + + override fun showEditHabitGroupScreen(habitGroup: HabitGroup) { + startActivity(IntentFactory().startEditGroupActivity(this@ShowHabitGroupActivity, habitGroup)) + } + + override fun showMessage(m: ShowHabitGroupMenuPresenter.Message?) { + when (m) { + ShowHabitGroupMenuPresenter.Message.COULD_NOT_EXPORT -> { + showMessage(resources.getString(R.string.could_not_export)) + } + else -> {} + } + } + + override fun showSendFileScreen(filename: String) { + this@ShowHabitGroupActivity.showSendFileScreen(filename) + } + + override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) { + ConfirmDeleteDialog(this@ShowHabitGroupActivity, callback, 1).dismissCurrentAndShow() + } + + override fun close() { + this@ShowHabitGroupActivity.finish() + } + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupMenu.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupMenu.kt new file mode 100644 index 000000000..4ee4eedb5 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupMenu.kt @@ -0,0 +1,32 @@ +package org.isoron.uhabits.activities.habits.show + +import android.view.Menu +import android.view.MenuItem +import org.isoron.uhabits.R +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitGroupMenuPresenter + +class ShowHabitGroupMenu( + val activity: ShowHabitGroupActivity, + val presenter: ShowHabitGroupMenuPresenter, + val preferences: Preferences +) { + fun onCreateOptionsMenu(menu: Menu): Boolean { + activity.menuInflater.inflate(R.menu.show_habit_group, menu) + return true + } + + fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_edit_habit_group -> { + presenter.onEditHabit() + return true + } + R.id.action_delete -> { + presenter.onDeleteHabit() + return true + } + } + return false + } +} diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupView.kt new file mode 100644 index 000000000..8575a38f5 --- /dev/null +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupView.kt @@ -0,0 +1,35 @@ +package org.isoron.uhabits.activities.habits.show + +import android.content.Context +import android.view.LayoutInflater +import android.widget.FrameLayout +import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitGroupPresenter +import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitGroupState +import org.isoron.uhabits.databinding.ShowHabitGroupBinding +import org.isoron.uhabits.utils.setupToolbar + +class ShowHabitGroupView(context: Context) : FrameLayout(context) { + private val binding = ShowHabitGroupBinding.inflate(LayoutInflater.from(context)) + + init { + addView(binding.root) + } + + fun setState(data: ShowHabitGroupState) { + setupToolbar( + binding.toolbar, + title = data.title, + color = data.color, + theme = data.theme + ) + binding.subtitleCard.setState(data.subtitle) + binding.overviewCard.setState(data.overview) + binding.notesCard.setState(data.notes) + binding.streakCard.setState(data.streaks) + binding.scoreCard.setState(data.scores) + } + + fun setListener(presenter: ShowHabitGroupPresenter) { + binding.scoreCard.setListener(presenter.scoreCardPresenter) + } +} 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 b2beb3d46..866bfe11c 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 @@ -27,6 +27,7 @@ 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.habits.show.ShowHabitGroupActivity import org.isoron.uhabits.activities.intro.IntroActivity import org.isoron.uhabits.activities.settings.SettingsActivity import org.isoron.uhabits.core.models.Habit @@ -67,6 +68,11 @@ class IntentFactory data = Uri.parse(habit.uriString) } + fun startShowHabitGroupActivity(context: Context, habitGroup: HabitGroup) = + Intent(context, ShowHabitGroupActivity::class.java).apply { + data = Uri.parse(habitGroup.uriString) + } + fun viewFAQ(context: Context) = buildViewIntent(context.getString(R.string.helpURL)) diff --git a/uhabits-android/src/main/res/menu/show_habit_group.xml b/uhabits-android/src/main/res/menu/show_habit_group.xml new file mode 100644 index 000000000..6f1e43a82 --- /dev/null +++ b/uhabits-android/src/main/res/menu/show_habit_group.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file 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 494209fce..d5df21f1a 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 @@ -74,12 +74,12 @@ class StreakList { from: Timestamp, to: Timestamp ) { + if (habitList.isEmpty) return var current = from var streakRunning = false var streakStart = from while (current <= to) { - if (habitList.all { it.streaks.isInStreaks(current) } && !streakRunning - ) { + if (habitList.all { it.streaks.isInStreaks(current) } && !streakRunning) { streakStart = current streakRunning = true } else if (streakRunning) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index b66b08be6..73ac30d23 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -22,6 +22,7 @@ import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitType import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST @@ -51,6 +52,10 @@ open class ListHabitsBehavior @Inject constructor( screen.showHabitScreen(h) } + fun onClickHabitGroup(hgr: HabitGroup) { + screen.showHabitGroupScreen(hgr) + } + fun onEdit(habit: Habit, timestamp: Timestamp?) { val entry = habit.computedEntries.get(timestamp!!) if (habit.type == HabitType.NUMERICAL) { @@ -178,6 +183,7 @@ open class ListHabitsBehavior @Inject constructor( interface Screen { fun showHabitScreen(h: Habit) + fun showHabitGroupScreen(hgr: HabitGroup) fun showIntroScreen() fun showMessage(m: Message) fun showNumberPopup( diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroup.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroup.kt new file mode 100644 index 000000000..56359832e --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroup.kt @@ -0,0 +1,78 @@ +package org.isoron.uhabits.core.ui.screens.habits.show + +import org.isoron.uhabits.core.commands.CommandRunner +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.PaletteColor +import org.isoron.uhabits.core.preferences.Preferences +import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardPresenter +import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardState +import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardPresenter +import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardState +import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter +import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardState +import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCardState +import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCartPresenter +import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardPresenter +import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState +import org.isoron.uhabits.core.ui.views.Theme + +data class ShowHabitGroupState( + val title: String = "", + val color: PaletteColor = PaletteColor(1), + val subtitle: SubtitleCardState, + val overview: OverviewCardState, + val notes: NotesCardState, + val streaks: StreakCardState, + val scores: ScoreCardState, + val theme: Theme +) + +class ShowHabitGroupPresenter( + val habitGroup: HabitGroup, + val preferences: Preferences, + val screen: Screen, + val commandRunner: CommandRunner +) { + val scoreCardPresenter = ScoreCardPresenter( + preferences = preferences, + screen = screen + ) + + companion object { + fun buildState( + habitGroup: HabitGroup, + preferences: Preferences, + theme: Theme + ): ShowHabitGroupState { + return ShowHabitGroupState( + title = habitGroup.name, + color = habitGroup.color, + theme = theme, + subtitle = SubtitleCardPresenter.buildState( + habitGroup = habitGroup, + theme = theme + ), + overview = OverviewCardPresenter.buildState( + habitGroup = habitGroup, + theme = theme + ), + notes = NotesCardPresenter.buildState( + habitGroup = habitGroup + ), + streaks = StreakCartPresenter.buildState( + habitGroup = habitGroup, + theme = theme + ), + scores = ScoreCardPresenter.buildState( + spinnerPosition = preferences.scoreCardSpinnerPosition, + habitGroup = habitGroup, + firstWeekday = preferences.firstWeekdayInt, + theme = theme + ) + ) + } + } + + interface Screen : + ScoreCardPresenter.Screen +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroupMenuPresenter.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroupMenuPresenter.kt new file mode 100644 index 000000000..196d9a8a1 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroupMenuPresenter.kt @@ -0,0 +1,46 @@ +package org.isoron.uhabits.core.ui.screens.habits.show + +import org.isoron.uhabits.core.commands.CommandRunner +import org.isoron.uhabits.core.commands.DeleteHabitGroupsCommand +import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.HabitGroupList +import org.isoron.uhabits.core.tasks.TaskRunner +import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback +import java.io.File + +class ShowHabitGroupMenuPresenter( + private val commandRunner: CommandRunner, + private val habitGroup: HabitGroup, + private val habitGroupList: HabitGroupList, + private val screen: Screen, + private val system: System, + private val taskRunner: TaskRunner +) { + fun onEditHabit() { + screen.showEditHabitGroupScreen(habitGroup) + } + + fun onDeleteHabit() { + screen.showDeleteConfirmationScreen { + commandRunner.run(DeleteHabitGroupsCommand(habitGroupList, listOf(habitGroup))) + screen.close() + } + } + + enum class Message { + COULD_NOT_EXPORT + } + + interface Screen { + fun showEditHabitGroupScreen(habitGroup: HabitGroup) + fun showMessage(m: Message?) + fun showSendFileScreen(filename: String) + fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) + fun close() + fun refresh() + } + + interface System { + fun getCSVOutputDir(): File + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/NotesCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/NotesCard.kt index 47049bb83..8f608b98b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/NotesCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/NotesCard.kt @@ -20,6 +20,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup data class NotesCardState( val description: String @@ -30,5 +31,9 @@ class NotesCardPresenter { fun buildState(habit: Habit) = NotesCardState( description = habit.description ) + + fun buildState(habitGroup: HabitGroup) = NotesCardState( + description = habitGroup.description + ) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/OverviewCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/OverviewCard.kt index e15c08403..d895b7523 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/OverviewCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/OverviewCard.kt @@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.ui.views.Theme import org.isoron.uhabits.core.utils.DateUtils @@ -57,5 +58,27 @@ class OverviewCardPresenter { theme = theme ) } + + fun buildState(habitGroup: HabitGroup, theme: Theme): OverviewCardState { + val today = DateUtils.getTodayWithOffset() + val lastMonth = today.minus(30) + val lastYear = today.minus(365) + val scores = habitGroup.scores + val scoreToday = scores[today].value.toFloat() + val scoreLastMonth = scores[lastMonth].value.toFloat() + val scoreLastYear = scores[lastYear].value.toFloat() + val totalCount = habitGroup.habitList.sumOf { habit -> + habit.originalEntries.getKnown().count { it.value == Entry.YES_MANUAL } + .toLong() + } + return OverviewCardState( + color = habitGroup.color, + scoreToday = scoreToday, + scoreMonthDiff = scoreToday - scoreLastMonth, + scoreYearDiff = scoreToday - scoreLastYear, + totalCount = totalCount, + theme = theme + ) + } } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/ScoreCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/ScoreCard.kt index b85d75204..511bb757a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/ScoreCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/ScoreCard.kt @@ -20,6 +20,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.Score import org.isoron.uhabits.core.preferences.Preferences @@ -83,6 +84,45 @@ class ScoreCardPresenter( theme = theme ) } + + fun buildState( + habitGroup: HabitGroup, + firstWeekday: Int, + spinnerPosition: Int, + theme: Theme + ): ScoreCardState { + val bucketSize = BUCKET_SIZES[spinnerPosition] + val today = DateUtils.getTodayWithOffset() + val oldest = if (habitGroup.habitList.isEmpty) { + today + } else { + habitGroup.habitList.minOf { + it.computedEntries.getKnown().lastOrNull()?.timestamp ?: today + } + } + + val field = getTruncateField(bucketSize) + val scores = habitGroup.scores.getByInterval(oldest, today).groupBy { + DateUtils.truncate(field, it.timestamp, firstWeekday) + }.map { (timestamp, scores) -> + Score( + timestamp, + scores.map { + it.value + }.average() + ) + }.sortedBy { + it.timestamp + }.reversed() + + return ScoreCardState( + color = habitGroup.color, + scores = scores, + bucketSize = bucketSize, + spinnerPosition = spinnerPosition, + theme = theme + ) + } } fun onSpinnerPosition(position: Int) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/StreakCart.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/StreakCart.kt index a242ab4b7..cb78e19b8 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/StreakCart.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/StreakCart.kt @@ -20,6 +20,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.Streak import org.isoron.uhabits.core.ui.views.Theme @@ -39,5 +40,13 @@ class StreakCartPresenter { theme = theme ) } + + fun buildState(habitGroup: HabitGroup, theme: Theme): StreakCardState { + return StreakCardState( + color = habitGroup.color, + bestStreaks = habitGroup.streaks.getBest(10), + theme = theme + ) + } } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt index fb839933c..e9b569c61 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/SubtitleCard.kt @@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.Reminder @@ -54,5 +55,17 @@ class SubtitleCardPresenter { unit = habit.unit, theme = theme ) + + fun buildState( + habitGroup: HabitGroup, + theme: Theme + ): SubtitleCardState = SubtitleCardState( + color = habitGroup.color, + frequency = Frequency.DAILY, + isNumerical = false, + question = habitGroup.question, + reminder = habitGroup.reminder, + theme = theme + ) } } From 0a1cdd45cbda08f4fec82107076f061e3d176496 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Wed, 3 Jul 2024 10:30:57 +0200 Subject: [PATCH 18/51] Implement edit habit group activity --- .../habits/edit/EditHabitGroupActivity.kt | 2 +- .../habits/list/ListHabitsScreen.kt | 12 ++++ .../habits/list/ListHabitsSelectionMenu.kt | 2 +- .../habits/list/views/HabitCardListAdapter.kt | 3 +- .../habits/show/ShowHabitGroupActivity.kt | 6 +- .../habits/show/ShowHabitGroupMenu.kt | 4 +- .../list/ListHabitsSelectionMenuBehavior.kt | 62 +++++++++++++++---- .../show/ShowHabitGroupMenuPresenter.kt | 9 +-- .../ListHabitsSelectionMenuBehaviorTest.kt | 22 +++---- 9 files changed, 82 insertions(+), 40 deletions(-) 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 index 50fc5ef78..03761f26c 100644 --- 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 @@ -56,7 +56,7 @@ class EditHabitGroupActivity : AppCompatActivity() { if (intent.hasExtra("habitGroupId")) { binding.toolbar.title = getString(R.string.edit_habit_group) - habitGroupId = intent.getLongExtra("habitId", -1) + habitGroupId = intent.getLongExtra("habitGroupId", -1) val hgr = component.habitGroupList.getById(habitGroupId)!! color = hgr.color hgr.reminder?.let { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt index 4441cdbdc..cd10b4285 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt @@ -41,6 +41,7 @@ import org.isoron.uhabits.core.commands.ChangeHabitColorCommand import org.isoron.uhabits.core.commands.Command import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateHabitCommand +import org.isoron.uhabits.core.commands.DeleteHabitGroupsCommand import org.isoron.uhabits.core.commands.DeleteHabitsCommand import org.isoron.uhabits.core.commands.EditHabitCommand import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand @@ -179,6 +180,11 @@ class ListHabitsScreen activity.startActivity(intent) } + override fun showEditHabitGroupScreen(selected: List) { + val intent = intentFactory.startEditGroupActivity(activity, selected[0]) + activity.startActivity(intent) + } + override fun showFAQScreen() { val intent = intentFactory.viewFAQ(activity) activity.startActivity(intent) @@ -318,6 +324,12 @@ class ListHabitsScreen command.selected.size ) } + is DeleteHabitGroupsCommand -> { + return activity.resources.getQuantityString( + R.plurals.toast_habits_deleted, + command.selected.size + ) + } is EditHabitCommand -> { return activity.resources.getQuantityString(R.plurals.toast_habits_changed, 1) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt index 03e50fa6d..bf54b3ce6 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt @@ -82,7 +82,7 @@ class ListHabitsSelectionMenu @Inject constructor( itemArchive.isVisible = behavior.canArchive() itemUnarchive.isVisible = behavior.canUnarchive() itemNotify.isVisible = prefs.isDeveloper - activeActionMode?.title = listAdapter.selectedHabits.size.toString() + activeActionMode?.title = (listAdapter.selectedHabits.size + listAdapter.selectedHabitGroups.size).toString() return true } override fun onDestroyActionMode(mode: ActionMode?) { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt index 264978c66..8d1080ea5 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt @@ -79,11 +79,12 @@ class HabitCardListAdapter @Inject constructor( */ override fun clearSelection() { selectedHabits.clear() + selectedHabitGroups.clear() notifyDataSetChanged() observable.notifyListeners() } - override fun getSelected(): List { + override fun getSelectedHabits(): List { return ArrayList(selectedHabits) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupActivity.kt index 97bab67c2..d204f7a48 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupActivity.kt @@ -8,11 +8,9 @@ import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.isoron.uhabits.AndroidDirFinder import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.R import org.isoron.uhabits.activities.AndroidThemeSwitcher -import org.isoron.uhabits.activities.HabitGroupsDirFinder import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog import org.isoron.uhabits.core.commands.Command import org.isoron.uhabits.core.commands.CommandRunner @@ -68,9 +66,7 @@ class ShowHabitGroupActivity : AppCompatActivity(), CommandRunner.Listener { commandRunner = commandRunner, habitGroup = habitGroup, habitGroupList = habitGroupList, - screen = screen, - system = HabitGroupsDirFinder(AndroidDirFinder(this)), - taskRunner = appComponent.taskRunner + screen = screen ) menu = ShowHabitGroupMenu( diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupMenu.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupMenu.kt index 4ee4eedb5..1dc4bbd03 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupMenu.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitGroupMenu.kt @@ -19,11 +19,11 @@ class ShowHabitGroupMenu( fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_edit_habit_group -> { - presenter.onEditHabit() + presenter.onEditHabitGroup() return true } R.id.action_delete -> { - presenter.onDeleteHabit() + presenter.onDeleteHabitGroup() return true } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt index 49df8afab..231fa4c45 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt @@ -18,13 +18,17 @@ */ package org.isoron.uhabits.core.ui.screens.habits.list +import org.isoron.uhabits.core.commands.ArchiveHabitGroupsCommand import org.isoron.uhabits.core.commands.ArchiveHabitsCommand import org.isoron.uhabits.core.commands.ChangeHabitColorCommand +import org.isoron.uhabits.core.commands.ChangeHabitGroupColorCommand import org.isoron.uhabits.core.commands.CommandRunner +import org.isoron.uhabits.core.commands.DeleteHabitGroupsCommand import org.isoron.uhabits.core.commands.DeleteHabitsCommand import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.HabitGroupList import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback @@ -33,33 +37,55 @@ import javax.inject.Inject class ListHabitsSelectionMenuBehavior @Inject constructor( private val habitList: HabitList, + private val habitGroupList: HabitGroupList, private val screen: Screen, private val adapter: Adapter, var commandRunner: CommandRunner ) { fun canArchive(): Boolean { - for (habit in adapter.getSelected()) if (habit.isArchived) return false + for (habit in adapter.getSelectedHabits()) if (habit.isArchived) return false + for (hgr in adapter.getSelectedHabitGroups()) if (hgr.isArchived) return false return true } fun canEdit(): Boolean { - return adapter.getSelected().size == 1 + return (adapter.getSelectedHabits().size + adapter.getSelectedHabitGroups().size == 1) } fun canUnarchive(): Boolean { - for (habit in adapter.getSelected()) if (!habit.isArchived) return false + for (habit in adapter.getSelectedHabits()) if (!habit.isArchived) return false + for (hgr in adapter.getSelectedHabitGroups()) if (!hgr.isArchived) return false return true } fun onArchiveHabits() { - commandRunner.run(ArchiveHabitsCommand(habitList, adapter.getSelected())) + commandRunner.run(ArchiveHabitsCommand(habitList, adapter.getSelectedHabits())) + commandRunner.run(ArchiveHabitGroupsCommand(habitGroupList, adapter.getSelectedHabitGroups())) adapter.clearSelection() } fun onChangeColor() { - val (color) = adapter.getSelected()[0] + val color = if (adapter.getSelectedHabits().isNotEmpty()) { + adapter.getSelectedHabits()[0].color + } else { + adapter.getSelectedHabitGroups()[0].color + } + screen.showColorPicker(color) { selectedColor: PaletteColor -> - commandRunner.run(ChangeHabitColorCommand(habitList, adapter.getSelected(), selectedColor)) + commandRunner.run( + ChangeHabitColorCommand( + habitList, + adapter.getSelectedHabits(), + selectedColor + ) + ) + commandRunner.run( + ChangeHabitGroupColorCommand( + habitGroupList, + adapter.getSelectedHabitGroups(), + selectedColor + ) + ) adapter.clearSelection() } } @@ -67,28 +93,36 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( fun onDeleteHabits() { screen.showDeleteConfirmationScreen( { - adapter.performRemove(adapter.getSelected()) - commandRunner.run(DeleteHabitsCommand(habitList, adapter.getSelected())) + adapter.performRemove(adapter.getSelectedHabits()) + adapter.performRemoveHabitGroup(adapter.getSelectedHabitGroups()) + commandRunner.run(DeleteHabitsCommand(habitList, adapter.getSelectedHabits())) + commandRunner.run(DeleteHabitGroupsCommand(habitGroupList, adapter.getSelectedHabitGroups())) adapter.clearSelection() }, - adapter.getSelected().size + adapter.getSelectedHabits().size + adapter.getSelectedHabitGroups().size ) } fun onEditHabits() { - val selected = adapter.getSelected() - if (selected.isNotEmpty()) screen.showEditHabitsScreen(selected) + val selected = adapter.getSelectedHabits() + if (selected.isNotEmpty()) { + screen.showEditHabitsScreen(selected) + } else { + val selectedGroup = adapter.getSelectedHabitGroups() + screen.showEditHabitGroupScreen(selectedGroup) + } + adapter.clearSelection() } fun onUnarchiveHabits() { - commandRunner.run(UnarchiveHabitsCommand(habitList, adapter.getSelected())) + commandRunner.run(UnarchiveHabitsCommand(habitList, adapter.getSelectedHabits())) adapter.clearSelection() } interface Adapter { fun clearSelection() - fun getSelected(): List + fun getSelectedHabits(): List fun getSelectedHabitGroups(): List fun performRemove(selected: List) fun performRemoveHabitGroup(selected: List) @@ -106,5 +140,7 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( ) fun showEditHabitsScreen(selected: List) + + fun showEditHabitGroupScreen(selected: List) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroupMenuPresenter.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroupMenuPresenter.kt index 196d9a8a1..0d468dd5e 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroupMenuPresenter.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitGroupMenuPresenter.kt @@ -4,7 +4,6 @@ import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.DeleteHabitGroupsCommand import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.HabitGroupList -import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback import java.io.File @@ -12,15 +11,13 @@ class ShowHabitGroupMenuPresenter( private val commandRunner: CommandRunner, private val habitGroup: HabitGroup, private val habitGroupList: HabitGroupList, - private val screen: Screen, - private val system: System, - private val taskRunner: TaskRunner + private val screen: Screen ) { - fun onEditHabit() { + fun onEditHabitGroup() { screen.showEditHabitGroupScreen(habitGroup) } - fun onDeleteHabit() { + fun onDeleteHabitGroup() { screen.showDeleteConfirmationScreen { commandRunner.run(DeleteHabitGroupsCommand(habitGroupList, listOf(habitGroup))) screen.close() diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehaviorTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehaviorTest.kt index b60ee037c..edb08d934 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehaviorTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehaviorTest.kt @@ -52,27 +52,27 @@ class ListHabitsSelectionMenuBehaviorTest : BaseUnitTest() { @Test @Throws(Exception::class) fun canArchive() { - whenever(adapter.getSelected()).thenReturn(listOf(habit1, habit2)) + whenever(adapter.getSelectedHabits()).thenReturn(listOf(habit1, habit2)) assertFalse(behavior.canArchive()) - whenever(adapter.getSelected()).thenReturn(listOf(habit2, habit3)) + whenever(adapter.getSelectedHabits()).thenReturn(listOf(habit2, habit3)) assertTrue(behavior.canArchive()) } @Test @Throws(Exception::class) fun canEdit() { - whenever(adapter.getSelected()).thenReturn(listOf(habit1)) + whenever(adapter.getSelectedHabits()).thenReturn(listOf(habit1)) assertTrue(behavior.canEdit()) - whenever(adapter.getSelected()).thenReturn(listOf(habit1, habit2)) + whenever(adapter.getSelectedHabits()).thenReturn(listOf(habit1, habit2)) assertFalse(behavior.canEdit()) } @Test @Throws(Exception::class) fun canUnarchive() { - whenever(adapter.getSelected()).thenReturn(listOf(habit1, habit2)) + whenever(adapter.getSelectedHabits()).thenReturn(listOf(habit1, habit2)) assertFalse(behavior.canUnarchive()) - whenever(adapter.getSelected()).thenReturn(listOf(habit1)) + whenever(adapter.getSelectedHabits()).thenReturn(listOf(habit1)) assertTrue(behavior.canUnarchive()) } @@ -80,7 +80,7 @@ class ListHabitsSelectionMenuBehaviorTest : BaseUnitTest() { @Throws(Exception::class) fun onArchiveHabits() { assertFalse(habit2.isArchived) - whenever(adapter.getSelected()).thenReturn(listOf(habit2)) + whenever(adapter.getSelectedHabits()).thenReturn(listOf(habit2)) behavior.onArchiveHabits() assertTrue(habit2.isArchived) } @@ -90,7 +90,7 @@ class ListHabitsSelectionMenuBehaviorTest : BaseUnitTest() { fun onChangeColor() { assertThat(habit1.color, equalTo(PaletteColor(8))) assertThat(habit2.color, equalTo(PaletteColor(8))) - whenever(adapter.getSelected()).thenReturn(listOf(habit1, habit2)) + whenever(adapter.getSelectedHabits()).thenReturn(listOf(habit1, habit2)) behavior.onChangeColor() verify(screen) .showColorPicker(eq(PaletteColor(8)), colorPickerCallback.capture()) @@ -103,7 +103,7 @@ class ListHabitsSelectionMenuBehaviorTest : BaseUnitTest() { fun onDeleteHabits() { val id = habit1.id!! habitList.getById(id)!! - whenever(adapter.getSelected()).thenReturn(listOf(habit1)) + whenever(adapter.getSelectedHabits()).thenReturn(listOf(habit1)) behavior.onDeleteHabits() verify(screen).showDeleteConfirmationScreen(deleteCallback.capture(), eq(1)) deleteCallback.lastValue.onConfirmed() @@ -114,7 +114,7 @@ class ListHabitsSelectionMenuBehaviorTest : BaseUnitTest() { @Throws(Exception::class) fun onEditHabits() { val selected: List = listOf(habit1, habit2) - whenever(adapter.getSelected()).thenReturn(selected) + whenever(adapter.getSelectedHabits()).thenReturn(selected) behavior.onEditHabits() verify(screen).showEditHabitsScreen(selected) } @@ -123,7 +123,7 @@ class ListHabitsSelectionMenuBehaviorTest : BaseUnitTest() { @Throws(Exception::class) fun onUnarchiveHabits() { assertTrue(habit1.isArchived) - whenever(adapter.getSelected()).thenReturn(listOf(habit1)) + whenever(adapter.getSelectedHabits()).thenReturn(listOf(habit1)) behavior.onUnarchiveHabits() assertFalse(habit1.isArchived) } From 35c9a1a0aba5439a5bcb037c5bab9ebb2ebd3ae5 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Fri, 5 Jul 2024 15:12:20 +0200 Subject: [PATCH 19/51] Implement listing sub habits --- .../habits/list/views/HabitCardListAdapter.kt | 23 +- .../habits/list/views/HabitCardView.kt | 15 +- .../isoron/uhabits/core/models/HabitList.kt | 14 + .../models/memory/MemoryHabitGroupList.kt | 6 +- .../core/models/memory/MemoryHabitList.kt | 11 + .../core/models/sqlite/SQLiteHabitList.kt | 13 + .../screens/habits/list/HabitCardListCache.kt | 515 ++++++++++++------ .../org/isoron/uhabits/core/BaseUnitTest.kt | 3 + .../habits/list/HabitCardListCacheTest.kt | 6 +- .../ListHabitsSelectionMenuBehaviorTest.kt | 1 + 10 files changed, 432 insertions(+), 175 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt index 8d1080ea5..27ae9be1a 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.activities.habits.list.views +import android.annotation.SuppressLint import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView.Adapter import org.isoron.uhabits.activities.habits.list.MAX_CHECKMARK_COUNT @@ -74,9 +75,14 @@ class HabitCardListAdapter @Inject constructor( return cache.hasNoHabitGroup() } + fun hasNoSubHabits(): Boolean { + return cache.hasNoSubHabits() + } + /** * Sets all items as not selected. */ + @SuppressLint("NotifyDataSetChanged") override fun clearSelection() { selectedHabits.clear() selectedHabitGroups.clear() @@ -116,7 +122,7 @@ class HabitCardListAdapter @Inject constructor( } override fun getItemId(position: Int): Long { - val uuidString = getItemUUID(position) + val uuidString = cache.getUUIDByPosition(position) return if (uuidString != null) { val formattedUUIDString = formatUUID(uuidString) val uuid = UUID.fromString(formattedUUIDString) @@ -126,18 +132,6 @@ class HabitCardListAdapter @Inject constructor( } } - fun getItemUUID(position: Int): String? { - val h = cache.getHabitByPosition(position) - val hgr = cache.getHabitGroupByPosition(position) - return if (h != null) { - h.uuid!! - } else if (hgr != null) { - hgr.uuid!! - } else { - null - } - } - private fun formatUUID(uuidString: String): String { return uuidString.substring(0, 8) + "-" + uuidString.substring(8, 12) + "-" + @@ -207,7 +201,7 @@ class HabitCardListAdapter @Inject constructor( // function to override getItemViewType and return the type of the view. The view can either be a HabitCardView or a HabitGroupCardView override fun getItemViewType(position: Int): Int { - return if (position < cache.habitCount) { + return if (cache.getHabitByPosition(position) != null) { 0 } else { 1 @@ -322,6 +316,7 @@ class HabitCardListAdapter @Inject constructor( * * @param position position of the item to be toggled */ + @SuppressLint("NotifyDataSetChanged") fun toggleSelection(position: Int) { val h = cache.getHabitByPosition(position) val hgr = cache.getHabitGroupByPosition(position) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index 596b25668..e047549b9 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -136,11 +136,10 @@ class HabitCardView( init { scoreRing = RingView(context).apply { val thickness = dp(3f) - val rightMargin = dp(8f).toInt() + val margin = dp(8f).toInt() val ringSize = dp(15f).toInt() - val leftMargin = if (habit?.isSubHabit() == true) dp(30f).toInt() else dp(8f).toInt() layoutParams = LinearLayout.LayoutParams(ringSize, ringSize).apply { - setMargins(leftMargin, 0, rightMargin, 0) + setMargins(margin, 0, margin, 0) gravity = Gravity.CENTER } setThickness(thickness) @@ -268,6 +267,16 @@ class HabitCardView( } scoreRing.apply { setColor(c) + if (h.isSubHabit()) { + val rightMargin = dp(8f).toInt() + val ringSize = dp(15f).toInt() + val leftMargin = + if (habit?.isSubHabit() == true) dp(30f).toInt() else dp(8f).toInt() + layoutParams = LinearLayout.LayoutParams(ringSize, ringSize).apply { + setMargins(leftMargin, 0, rightMargin, 0) + gravity = Gravity.CENTER + } + } } checkmarkPanel.apply { color = c diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt index 61db7ed76..54b19dac1 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt @@ -67,6 +67,20 @@ abstract class HabitList : Iterable { @Throws(IllegalArgumentException::class) abstract fun add(habit: Habit) + /** + * Inserts a new habit in the list at the given position. + * + * If the id of the habit is null, the list will assign it a new id, which + * is guaranteed to be unique in the scope of the list. If id is not null, + * the caller should make sure that the list does not already contain + * another habit with same id, otherwise a RuntimeException will be thrown. + * + * @param habit the habit to be inserted + * @throws IllegalArgumentException if the habit is already on the list. + */ + @Throws(IllegalArgumentException::class) + abstract fun add(position: Int, habit: Habit) + /** * Returns the habit with specified id. * 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 bc80617ae..983514a4b 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 @@ -195,7 +195,11 @@ class MemoryHabitGroupList : HabitGroupList { private fun loadFromParent() { checkNotNull(parent) list.clear() - for (h in parent!!) if (filter.matches(h)) list.add(h) + for (h in parent!!) { + if (filter.matches(h)) { + list.add(h) + } + } resort() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt index e0b9037a6..c17a113fb 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt @@ -77,6 +77,17 @@ class MemoryHabitList : HabitList { resort() } + @Synchronized + @Throws(IllegalArgumentException::class) + override fun add(position: Int, habit: Habit) { + throwIfHasParent() + require(!list.contains(habit)) { "habit already added" } + val id = habit.id + if (id != null && getById(id) != null) throw RuntimeException("duplicate id") + if (id == null) habit.id = list.size.toLong() + list.add(position, habit) + } + @Synchronized override fun getById(id: Long): Habit? { for (h in list) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt index f7ef12a96..c53144a90 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt @@ -64,6 +64,19 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory observable.notifyListeners() } + @Synchronized + override fun add(position: Int, habit: Habit) { + loadRecords() + habit.position = size() + val record = HabitRecord() + record.copyFrom(habit) + repository.save(record) + habit.id = record.id + (habit.originalEntries as SQLiteEntryList).habitId = record.id + list.add(position, habit) + observable.notifyListeners() + } + @Synchronized override fun getById(id: Long): Habit? { loadRecords() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index c18ea5320..0bab9a99c 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -53,8 +53,8 @@ import javax.inject.Inject */ @AppScope class HabitCardListCache @Inject constructor( - private val allHabits: HabitList, - private val allHabitGroups: HabitGroupList, + private val habits: HabitList, + private val habitGroups: HabitGroupList, private val commandRunner: CommandRunner, taskRunner: TaskRunner, logging: Logging @@ -68,6 +68,7 @@ class HabitCardListCache @Inject constructor( private val data: CacheData private var filteredHabits: HabitList private var filteredHabitGroups: HabitGroupList + private var filteredSubHabits: MutableList private val taskRunner: TaskRunner @Synchronized @@ -87,12 +88,17 @@ class HabitCardListCache @Inject constructor( @Synchronized fun hasNoHabit(): Boolean { - return allHabits.isEmpty + return habits.isEmpty } @Synchronized fun hasNoHabitGroup(): Boolean { - return allHabitGroups.isEmpty + return habitGroups.isEmpty + } + + @Synchronized + fun hasNoSubHabits(): Boolean { + return habitGroups.all { it.habitList.isEmpty } } /** @@ -103,11 +109,7 @@ class HabitCardListCache @Inject constructor( */ @Synchronized fun getHabitByPosition(position: Int): Habit? { - return if (position < 0 || position >= data.habits.size) { - null - } else { - data.habits[position] - } + return data.positionToHabit[position] } /** @@ -118,16 +120,21 @@ class HabitCardListCache @Inject constructor( */ @Synchronized fun getHabitGroupByPosition(position: Int): HabitGroup? { - return if (position < data.habits.size || position >= data.habits.size + data.habitGroups.size) { - null + return data.positionToHabitGroup[position] + } + + @Synchronized + fun getUUIDByPosition(position: Int): String? { + return if (data.positionTypes[position] == STANDALONE_HABIT || data.positionTypes[position] == SUB_HABIT) { + data.positionToHabit[position]!!.uuid } else { - data.habitGroups[position - data.habits.size] + data.positionToHabitGroup[position]!!.uuid } } @get:Synchronized val itemCount: Int - get() = habitCount + habitGroupCount + get() = habitCount + habitGroupCount + subHabitCount @get:Synchronized val habitCount: Int @@ -137,15 +144,20 @@ class HabitCardListCache @Inject constructor( val habitGroupCount: Int get() = data.habitGroups.size + @get:Synchronized + val subHabitCount: Int + get() = data.subHabits.sumOf { it.size() } + @get:Synchronized @set:Synchronized var primaryOrder: Order get() = filteredHabits.primaryOrder set(order) { - allHabits.primaryOrder = order + habits.primaryOrder = order + habitGroups.primaryOrder = order filteredHabits.primaryOrder = order - allHabitGroups.primaryOrder = order filteredHabitGroups.primaryOrder = order + filteredSubHabits.forEach { it.primaryOrder = order } refreshAllHabits() } @@ -154,16 +166,17 @@ class HabitCardListCache @Inject constructor( var secondaryOrder: Order get() = filteredHabits.secondaryOrder set(order) { - allHabits.secondaryOrder = order + habits.secondaryOrder = order + habitGroups.secondaryOrder = order filteredHabits.secondaryOrder = order - allHabitGroups.secondaryOrder = order filteredHabitGroups.secondaryOrder = order + filteredSubHabits.forEach { it.secondaryOrder = order } refreshAllHabits() } @Synchronized - fun getScore(habitUUID: String): Double { - return data.scores[habitUUID]!! + fun getScore(uuid: String): Double { + return data.scores[uuid]!! } @Synchronized @@ -201,42 +214,68 @@ class HabitCardListCache @Inject constructor( @Synchronized fun remove(uuid: String) { - val h = data.uuidToHabit[uuid] - if (h != null) { - val position = data.habits.indexOf(h) - data.habits.removeAt(position) - data.uuidToHabit.remove(uuid) - data.checkmarks.remove(uuid) - data.notes.remove(uuid) - data.scores.remove(uuid) - listener.onItemRemoved(position) - } else { + val type = data.positionTypes[data.uuidToPosition[uuid]!!] + if (type == STANDALONE_HABIT) { + val h = data.uuidToHabit[uuid] + if (h != null) { + val position = data.habits.indexOf(h) + data.habits.removeAt(position) + data.checkmarks.remove(uuid) + data.notes.remove(uuid) + data.scores.remove(uuid) + data.decrementPositions(position + 1, data.positionTypes.size) + listener.onItemRemoved(position) + } + } else if (type == SUB_HABIT) { + val h = data.uuidToHabit[uuid] + if (h != null) { + val position = data.uuidToPosition[uuid]!! + val hgrUUID = h.parentUUID + val hgr = data.uuidToHabitGroup[hgrUUID] + val hgrIdx = data.habitGroups.indexOf(hgr) + data.subHabits[hgrIdx].remove(h) + data.checkmarks.remove(uuid) + data.notes.remove(uuid) + data.scores.remove(uuid) + data.decrementPositions(position + 1, data.positionTypes.size) + listener.onItemRemoved(position) + } + } else if (type == HABIT_GROUP) { val hgr = data.uuidToHabitGroup[uuid] if (hgr != null) { - val position = data.habitGroups.indexOf(hgr) - data.habitGroups.removeAt(position) - data.uuidToHabitGroup.remove(uuid) - listener.onItemRemoved(position + data.habits.size) + val position = data.uuidToPosition[uuid]!! + val hgrIdx = data.habitGroups.indexOf(hgr) + + for (habit in data.subHabits[hgrIdx].reversed()) { + data.checkmarks.remove(habit.uuid) + data.notes.remove(habit.uuid) + data.scores.remove(habit.uuid) + listener.onItemRemoved(data.uuidToPosition[habit.uuid]!!) + } + data.subHabits.removeAt(hgrIdx) + data.habitGroups.removeAt(hgrIdx) + data.scores.remove(hgr.uuid) + data.rebuildPositions() + listener.onItemRemoved(position) } } } @Synchronized fun reorder(from: Int, to: Int) { - if (data.habits.size in (from + 1)..to || data.habits.size in (to + 1)..from) { - logger.error("reorder: from and to are in different sections") - return - } - if (from < data.habits.size) { - val fromHabit = data.habits[from] - data.habits.removeAt(from) - data.habits.add(to, fromHabit) + if (from == to) return + val uuid = if (data.positionTypes[from] == STANDALONE_HABIT) { + data.positionToHabit[from]!!.uuid } else { - val fromHabitGroup = data.habitGroups[from] - data.habitGroups.removeAt(from - data.habits.size) - data.habitGroups.add(to - data.habits.size, fromHabitGroup) + data.positionToHabitGroup[from]!!.uuid + } + if (data.positionTypes[from] == STANDALONE_HABIT) { + val habit = data.positionToHabit[from]!! + data.performMove(habit, from, to) + } else if (data.positionTypes[from] == HABIT_GROUP) { + val habitGroup = data.positionToHabitGroup[from]!! + data.performMove(habitGroup, from, to) } - listener.onItemMoved(from, to) } @Synchronized @@ -246,8 +285,11 @@ class HabitCardListCache @Inject constructor( @Synchronized fun setFilter(matcher: HabitMatcher) { - filteredHabits = allHabits.getFiltered(matcher) - filteredHabitGroups = allHabitGroups.getFiltered(matcher) + filteredHabits = habits.getFiltered(matcher) + filteredHabitGroups = habitGroups.getFiltered(matcher) + for (idx in filteredSubHabits.indices) { + filteredSubHabits[idx] = filteredSubHabits[idx].getFiltered(matcher) + } } @Synchronized @@ -272,6 +314,11 @@ class HabitCardListCache @Inject constructor( val uuidToHabitGroup: HashMap = HashMap() val habits: MutableList val habitGroups: MutableList + val subHabits: MutableList + val uuidToPosition: HashMap + val positionTypes: MutableList + val positionToHabit: HashMap + val positionToHabitGroup: HashMap val checkmarks: HashMap val scores: HashMap val notes: HashMap> @@ -327,14 +374,186 @@ class HabitCardListCache @Inject constructor( for (h in filteredHabits) { if (h.uuid == null) continue habits.add(h) - uuidToHabit[h.uuid] = h } for (hgr in filteredHabitGroups) { if (hgr.uuid == null) continue habitGroups.add(hgr) + val habitList = hgr.habitList + subHabits.add(habitList) + + for (h in habitList) { + if (h.uuid == null) continue + } + } + } + + @Synchronized + fun rebuildPositions() { + positionToHabit.clear() + positionToHabitGroup.clear() + uuidToPosition.clear() + positionTypes.clear() + var position = 0 + for (h in habits) { + uuidToHabit[h.uuid] = h + uuidToPosition[h.uuid] = position + positionToHabit[position] = h + positionTypes.add(STANDALONE_HABIT) + position++ + } + + for ((idx, hgr) in habitGroups.withIndex()) { uuidToHabitGroup[hgr.uuid] = hgr + uuidToPosition[hgr.uuid] = position + positionToHabitGroup[position] = hgr + positionTypes.add(HABIT_GROUP) + val habitList = subHabits[idx] + position++ + + for (h in habitList) { + uuidToHabit[h.uuid] = h + uuidToPosition[h.uuid] = position + positionToHabit[position] = h + positionTypes.add(SUB_HABIT) + position++ + } + } + } + + @Synchronized + fun isValidInsert(habit: Habit, position: Int): Boolean { + if (habit.parentUUID == null) { + return position <= habits.size + } else { + val parent = uuidToHabitGroup[habit.parentUUID] + if (parent == null) { + return false + } + val parentPosition = uuidToPosition[habit.parentUUID]!! + val parentIndex = habitGroups.indexOf(parent) + val nextGroup = habitGroups.getOrNull(parentIndex + 1) + val nextGroupPosition = uuidToPosition[nextGroup?.uuid] + return (position > parentPosition && position <= positionTypes.size) && (nextGroupPosition == null || position <= nextGroupPosition) + } + } + + @Synchronized + fun isValidInsert(habitGroup: HabitGroup, position: Int): Boolean { + return (position == positionTypes.size) || (positionTypes[position] == HABIT_GROUP) + } + + @Synchronized + fun incrementPositions(from: Int, to: Int) { + for (pos in positionToHabit.keys.sortedByDescending { it }) { + if (pos in from..to) { + positionToHabit[pos + 1] = positionToHabit[pos]!! + positionToHabit.remove(pos) + } + } + for (pos in positionToHabitGroup.keys.sortedByDescending { it }) { + if (pos in from..to) { + positionToHabitGroup[pos + 1] = positionToHabitGroup[pos]!! + positionToHabitGroup.remove(pos) + } + } + for ((key, pos) in uuidToPosition.entries) { + if (pos in from..to) { + uuidToPosition[key] = pos + 1 + } + } + } + + @Synchronized + fun decrementPositions(fromPosition: Int, toPosition: Int) { + positionTypes.removeAt(fromPosition) + for (pos in positionToHabit.keys.sortedBy { it }) { + if (pos in fromPosition..toPosition) { + positionToHabit[pos - 1] = positionToHabit[pos]!! + positionToHabit.remove(pos) + } + } + for (pos in positionToHabitGroup.keys.sortedBy { it }) { + if (pos in fromPosition..toPosition) { + positionToHabitGroup[pos - 1] = positionToHabitGroup[pos]!! + positionToHabitGroup.remove(pos) + } + } + for ((key, pos) in uuidToPosition.entries) { + if (pos in fromPosition..toPosition) { + uuidToPosition[key] = pos - 1 + } + } + } + + @Synchronized + fun performMove( + habit: Habit, + fromPosition: Int, + toPosition: Int + ) { + val type = positionTypes[fromPosition] + if (type == HABIT_GROUP) return + + // Workaround for https://github.com/iSoron/uhabits/issues/968 + val checkedToPosition = if (toPosition > positionTypes.size) { + logger.error("performMove: $toPosition for habit is strictly higher than ${habits.size}") + positionTypes.size + } else { + toPosition + } + + val verifyPosition = if (fromPosition > checkedToPosition) checkedToPosition else checkedToPosition + 1 + if (!isValidInsert(habit, verifyPosition)) return + + if (type == STANDALONE_HABIT) { + habits.removeAt(fromPosition) + if (fromPosition < checkedToPosition) { + decrementPositions(fromPosition + 1, checkedToPosition) + } else { + incrementPositions(toPosition, fromPosition - 1) + } + habits.add(checkedToPosition, habit) + positionTypes.add(checkedToPosition, STANDALONE_HABIT) + } else { + val hgr = uuidToHabitGroup[habit.parentUUID] + val hgrIdx = habitGroups.indexOf(hgr) + val h = positionToHabit[fromPosition]!! + subHabits[hgrIdx].remove(h) + if (fromPosition < checkedToPosition) { + decrementPositions(fromPosition + 1, checkedToPosition) + } else { + incrementPositions(toPosition, fromPosition - 1) + } + subHabits[hgrIdx].add(checkedToPosition - uuidToPosition[hgr!!.uuid]!! - 1, habit) + positionTypes.add(checkedToPosition, SUB_HABIT) } + + positionToHabit[checkedToPosition] = habit + uuidToPosition[habit.uuid] = checkedToPosition + listener.onItemMoved(fromPosition, checkedToPosition) + } + + @Synchronized + fun performMove( + habitGroup: HabitGroup, + fromPosition: Int, + toPosition: Int + ) { + if (positionTypes[fromPosition] != HABIT_GROUP) return + if (!isValidInsert(habitGroup, toPosition)) return + val fromIdx = habitGroups.indexOf(habitGroup) + val habitList = subHabits[fromIdx] + val toIdx = habitGroups.indexOf(positionToHabitGroup[toPosition]) - (if (fromPosition < toPosition) 1 else 0) + + habitGroups.removeAt(fromIdx) + subHabits.removeAt(fromIdx) + + habitGroups.add(toIdx, habitGroup) + subHabits.add(toIdx, habitList) + + rebuildPositions() + listener.onItemMoved(fromPosition, toPosition) } /** @@ -343,6 +562,11 @@ class HabitCardListCache @Inject constructor( init { habits = LinkedList() habitGroups = LinkedList() + subHabits = LinkedList() + positionTypes = LinkedList() + uuidToPosition = HashMap() + positionToHabit = HashMap() + positionToHabitGroup = HashMap() checkmarks = HashMap() scores = HashMap() notes = HashMap() @@ -374,35 +598,35 @@ class HabitCardListCache @Inject constructor( @Synchronized override fun doInBackground() { newData.fetchHabits() + newData.rebuildPositions() newData.copyScoresFrom(data) newData.copyCheckmarksFrom(data) newData.copyNoteIndicatorsFrom(data) val today = getTodayWithOffset() val dateFrom = today.minus(checkmarkCount - 1) if (runner != null) runner!!.publishProgress(this, -1) - for (position in newData.habits.indices) { + for ((position, type) in newData.positionTypes.withIndex()) { if (isCancelled) return - val habit = newData.habits[position] - if (targetUUID != null && targetUUID != habit.uuid) continue - newData.scores[habit.uuid] = habit.scores[today].value - val list: MutableList = ArrayList() - val notes: MutableList = ArrayList() - for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) { - list.add(value) - notes.add(note) + if (type == STANDALONE_HABIT || type == SUB_HABIT) { + val habit = newData.positionToHabit[position]!! + if (targetUUID != null && targetUUID != habit.uuid) continue + newData.scores[habit.uuid] = habit.scores[today].value + val list: MutableList = ArrayList() + val notes: MutableList = ArrayList() + for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) { + list.add(value) + notes.add(note) + } + val entries = list.toTypedArray() + newData.checkmarks[habit.uuid] = ArrayUtils.toPrimitive(entries) + newData.notes[habit.uuid] = notes.toTypedArray() + runner!!.publishProgress(this, position) + } else if (type == HABIT_GROUP) { + val habitGroup = newData.positionToHabitGroup[position]!! + if (targetUUID != null && targetUUID != habitGroup.uuid) continue + newData.scores[habitGroup.uuid] = habitGroup.scores[today].value + runner!!.publishProgress(this, position) } - val entries = list.toTypedArray() - newData.checkmarks[habit.uuid] = ArrayUtils.toPrimitive(entries) - newData.notes[habit.uuid] = notes.toTypedArray() - runner!!.publishProgress(this, position) - } - - for (position in newData.habitGroups.indices) { - if (isCancelled) return - val hgr = newData.habitGroups[position] - if (targetUUID != null && targetUUID != hgr.uuid) continue - newData.scores[hgr.uuid] = hgr.scores[today].value - runner!!.publishProgress(this, position + newData.habits.size) } } @@ -424,8 +648,21 @@ class HabitCardListCache @Inject constructor( @Synchronized private fun performInsert(habit: Habit, position: Int) { + if (!data.isValidInsert(habit, position)) return val uuid = habit.uuid - data.habits.add(position, habit) + if (habit.parentUUID == null) { + data.habits.add(position, habit) + data.positionTypes.add(position, STANDALONE_HABIT) + } else { +// val parent = data.uuidToHabitGroup[habit.parentUUID] +// val parentIdx = data.habitGroups.indexOf(parent) +// val parentPosition = data.uuidToPosition[habit.parentUUID]!! +// data.subHabits[parentIdx].add(position - parentPosition - 1, habit) + data.positionTypes.add(position, SUB_HABIT) + } + data.incrementPositions(position, data.positionTypes.size - 1) + data.positionToHabit[position] = habit + data.uuidToPosition[uuid] = position data.uuidToHabit[uuid] = habit data.scores[uuid] = newData.scores[uuid]!! data.checkmarks[uuid] = newData.checkmarks[uuid]!! @@ -435,62 +672,23 @@ class HabitCardListCache @Inject constructor( @Synchronized private fun performInsert(habitGroup: HabitGroup, position: Int) { - val newPosition = if (position < data.habits.size) { - data.habits.size - } else { - position - } + if (!data.isValidInsert(habitGroup, position)) return val uuid = habitGroup.uuid - data.habitGroups.add(newPosition - data.habits.size, habitGroup) - data.uuidToHabitGroup[uuid] = habitGroup - data.scores[uuid] = newData.scores[uuid]!! - listener.onItemInserted(newPosition) - } - - @Synchronized - private fun performMove( - habit: Habit, - fromPosition: Int, - toPosition: Int - ) { - data.habits.removeAt(fromPosition) + val prevIdx = newData.habitGroups.indexOf(habitGroup) + val habitList = newData.subHabits[prevIdx] + var idx = data.habitGroups.indexOf(data.positionToHabitGroup[position]) + if (idx < 0) idx = data.habitGroups.size - // Workaround for https://github.com/iSoron/uhabits/issues/968 - val checkedToPosition = if (toPosition > data.habits.size) { - logger.error("performMove: $toPosition for habit is strictly higher than ${data.habits.size}") - data.habits.size - } else { - toPosition - } - - data.habits.add(checkedToPosition, habit) - listener.onItemMoved(fromPosition, checkedToPosition) - } - - private fun performMove( - habitGroup: HabitGroup, - fromPosition: Int, - toPosition: Int - ) { - if (fromPosition < data.habits.size || fromPosition > data.habits.size + data.habitGroups.size) { - logger.error("performMove: $fromPosition for habit group is out of bounds") - return - } - data.habitGroups.removeAt(fromPosition - data.habits.size) - - // Workaround for https://github.com/iSoron/uhabits/issues/968 - val checkedToPosition = if (toPosition < data.habits.size) { - logger.error("performMove: $toPosition for habit group is strictly lower than ${data.habits.size}") - data.habits.size - } else if (toPosition > data.habits.size + data.habitGroups.size) { - logger.error("performMove: $toPosition for habit group is strictly higher than ${data.habits.size + data.habitGroups.size}") - data.habits.size + data.habitGroups.size - } else { - toPosition + data.habitGroups.add(idx, habitGroup) + data.subHabits.add(prevIdx, habitList) + data.scores[uuid] = newData.scores[uuid]!! + for (h in habitList) { + data.scores[h.uuid] = newData.scores[h.uuid]!! + data.checkmarks[h.uuid] = newData.checkmarks[h.uuid]!! + data.notes[h.uuid] = newData.notes[h.uuid]!! } - - data.habitGroups.add(checkedToPosition - data.habits.size, habitGroup) - listener.onItemMoved(fromPosition, checkedToPosition) + data.rebuildPositions() + listener.onItemInserted(position) } @Synchronized @@ -500,7 +698,7 @@ class HabitCardListCache @Inject constructor( val newScore = newData.scores[uuid]!! if (oldScore != newScore) unchanged = false - if (position < data.habits.size) { + if (data.positionTypes[position] != HABIT_GROUP) { val oldCheckmarks = data.checkmarks[uuid] val newCheckmarks = newData.checkmarks[uuid]!! val oldNoteIndicators = data.notes[uuid] @@ -519,38 +717,45 @@ class HabitCardListCache @Inject constructor( @Synchronized private fun processPosition(currentPosition: Int) { - if (currentPosition < newData.habits.size) { - val habit = newData.habits[currentPosition] - val uuid = habit.uuid - val prevPosition = data.habits.indexOf(habit) + val type = newData.positionTypes[currentPosition] + + if (type == STANDALONE_HABIT || type == SUB_HABIT) { + val habit = newData.positionToHabit[currentPosition]!! + val uuid = habit.uuid ?: throw NullPointerException() + val prevPosition = data.uuidToPosition[uuid] ?: -1 + val newPosition = if (type == STANDALONE_HABIT) { + currentPosition + } else { + val hgr = data.uuidToHabitGroup[habit.parentUUID] + val hgrIdx = data.habitGroups.indexOf(hgr) + newData.subHabits[hgrIdx].indexOf(habit) + data.uuidToPosition[hgr!!.uuid]!! + 1 + } if (prevPosition < 0) { - performInsert(habit, currentPosition) + performInsert(habit, newPosition) } else { - if (prevPosition != currentPosition) { - performMove( + if (prevPosition != newPosition) { + data.performMove( habit, prevPosition, - currentPosition + newPosition ) } - if (uuid == null) throw NullPointerException() performUpdate(uuid, currentPosition) } - } else { - val habitGroup = newData.habitGroups[currentPosition - data.habits.size] - val uuid = habitGroup.uuid - val prevPosition = data.habitGroups.indexOf(habitGroup) + data.habits.size - if (prevPosition < data.habits.size) { + } else if (type == HABIT_GROUP) { + val habitGroup = newData.positionToHabitGroup[currentPosition]!! + val uuid = habitGroup.uuid ?: throw NullPointerException() + val prevPosition = data.uuidToPosition[uuid] ?: -1 + if (prevPosition < 0) { performInsert(habitGroup, currentPosition) } else { if (prevPosition != currentPosition) { - performMove( + data.performMove( habitGroup, prevPosition, currentPosition ) } - if (uuid == null) throw NullPointerException() performUpdate(uuid, currentPosition) } } @@ -558,27 +763,29 @@ class HabitCardListCache @Inject constructor( @Synchronized private fun processRemovedHabits() { - val before: Set = data.uuidToHabit.keys - val after: Set = newData.uuidToHabit.keys + val before: Set = (data.uuidToHabit.keys).union(data.uuidToHabitGroup.keys) + val after: Set = (newData.uuidToHabit.keys).union(newData.uuidToHabitGroup.keys) val removed: MutableSet = TreeSet(before) removed.removeAll(after) - for (uuid in removed) remove(uuid!!) - processRemovedHabitGroups() + for (uuid in removed.sortedBy { uuid -> data.uuidToPosition[uuid] }) remove(uuid!!) } + } - @Synchronized - private fun processRemovedHabitGroups() { - val before: Set = data.uuidToHabitGroup.keys - val after: Set = newData.uuidToHabitGroup.keys - val removed: MutableSet = TreeSet(before) - removed.removeAll(after) - for (uuid in removed) remove(uuid!!) - } + companion object { + const val STANDALONE_HABIT = 0 + const val HABIT_GROUP = 1 + const val SUB_HABIT = 2 } init { - filteredHabits = allHabits - filteredHabitGroups = allHabitGroups + filteredHabits = habits + filteredHabitGroups = habitGroups + filteredSubHabits = LinkedList() + for (hgr in habitGroups) { + val subList = hgr.habitList + filteredSubHabits.add(subList) + } + this.taskRunner = taskRunner listener = object : Listener {} data = CacheData() diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/BaseUnitTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/BaseUnitTest.kt index b8fd183db..ca5ca5b6c 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/BaseUnitTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/BaseUnitTest.kt @@ -24,6 +24,7 @@ import org.isoron.uhabits.core.database.Database import org.isoron.uhabits.core.database.DatabaseOpener import org.isoron.uhabits.core.database.JdbcDatabase import org.isoron.uhabits.core.database.MigrationHelper +import org.isoron.uhabits.core.models.HabitGroupList import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.ModelFactory import org.isoron.uhabits.core.models.Timestamp @@ -52,6 +53,7 @@ import java.sql.SQLException @RunWith(MockitoJUnitRunner::class) open class BaseUnitTest { protected open lateinit var habitList: HabitList + protected open lateinit var habitGroupList: HabitGroupList protected lateinit var fixtures: HabitFixtures protected lateinit var modelFactory: ModelFactory protected lateinit var taskRunner: SingleThreadTaskRunner @@ -80,6 +82,7 @@ open class BaseUnitTest { setStartDayOffset(0, 0) val memoryModelFactory = MemoryModelFactory() habitList = spy(memoryModelFactory.buildHabitList()) + habitGroupList = spy(memoryModelFactory.buildHabitGroupList()) fixtures = HabitFixtures(memoryModelFactory, habitList) modelFactory = memoryModelFactory taskRunner = SingleThreadTaskRunner() diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.kt index e3ff88c18..264a64c6c 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.kt @@ -43,7 +43,7 @@ class HabitCardListCacheTest : BaseUnitTest() { for (i in 0..9) { if (i == 3) habitList.add(fixtures.createLongHabit()) else habitList.add(fixtures.createShortHabit()) } - cache = HabitCardListCache(habitList, commandRunner, taskRunner, mock()) + cache = HabitCardListCache(habitList, habitGroupList, commandRunner, taskRunner, mock()) cache.setCheckmarkCount(10) cache.refreshAllHabits() cache.onAttached() @@ -82,8 +82,8 @@ class HabitCardListCacheTest : BaseUnitTest() { val h = habitList.getByPosition(3) val score = h.scores[today].value assertThat(cache.getHabitByPosition(3), equalTo(h)) - assertThat(cache.getScore(h.id!!), equalTo(score)) - val actualCheckmarks = cache.getCheckmarks(h.id!!) + assertThat(cache.getScore(h.uuid!!), equalTo(score)) + val actualCheckmarks = cache.getCheckmarks(h.uuid!!) val expectedCheckmarks = h .computedEntries diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehaviorTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehaviorTest.kt index edb08d934..5f5d86696 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehaviorTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehaviorTest.kt @@ -140,6 +140,7 @@ class ListHabitsSelectionMenuBehaviorTest : BaseUnitTest() { habitList.add(habit3) behavior = ListHabitsSelectionMenuBehavior( habitList, + habitGroupList, screen, adapter, commandRunner From 6ff9c2da619998e0234fe4812e79756174939033 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Fri, 5 Jul 2024 15:31:41 +0200 Subject: [PATCH 20/51] Implement show sub habits --- .../activities/habits/show/ShowHabitActivity.kt | 12 +++++++++--- .../java/org/isoron/uhabits/intents/IntentFactory.kt | 10 ++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt index 14baee148..1b0e8c6a9 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt @@ -18,7 +18,6 @@ */ package org.isoron.uhabits.activities.habits.show -import android.content.ContentUris import android.os.Bundle import android.view.HapticFeedbackConstants import android.view.Menu @@ -74,8 +73,15 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener { super.onCreate(savedInstanceState) val appComponent = (applicationContext as HabitsApplication).component - val habitList = appComponent.habitList - habit = habitList.getById(ContentUris.parseId(intent.data!!))!! + val habitGroupList = appComponent.habitGroupList + val parentUUID = intent.getStringExtra("parentUUID") + val habitList = if (parentUUID == null) { + appComponent.habitList + } else { + habitGroupList.getByUUID(parentUUID)!!.habitList + } + val uuid = intent.getStringExtra("habitUUID")!! + habit = habitList.getByUUID(uuid)!! preferences = appComponent.preferences commandRunner = appComponent.commandRunner widgetUpdater = appComponent.widgetUpdater 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 866bfe11c..6aed4f7c1 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 @@ -63,10 +63,12 @@ class IntentFactory fun startSettingsActivity(context: Context) = Intent(context, SettingsActivity::class.java) - fun startShowHabitActivity(context: Context, habit: Habit) = - Intent(context, ShowHabitActivity::class.java).apply { - data = Uri.parse(habit.uriString) - } + fun startShowHabitActivity(context: Context, habit: Habit): Intent { + val intent = Intent(context, ShowHabitActivity::class.java) + intent.putExtra("habitUUID", habit.uuid) + intent.putExtra("parentUUID", habit.parentUUID) + return intent + } fun startShowHabitGroupActivity(context: Context, habitGroup: HabitGroup) = Intent(context, ShowHabitGroupActivity::class.java).apply { From b043c90770fdbefb7a919150d71cc6245095de3d Mon Sep 17 00:00:00 2001 From: Dharanish Date: Fri, 5 Jul 2024 16:02:36 +0200 Subject: [PATCH 21/51] Implement edits sub habits --- .../habits/edit/EditHabitActivity.kt | 35 +++++++++++-------- .../isoron/uhabits/intents/IntentFactory.kt | 3 +- .../uhabits/core/commands/EditHabitCommand.kt | 4 +-- .../isoron/uhabits/core/io/LoopDBImporter.kt | 2 +- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt index d5cc898b1..8c70d3cb5 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt @@ -75,7 +75,7 @@ class EditHabitActivity : AppCompatActivity() { private lateinit var binding: ActivityEditHabitBinding private lateinit var commandRunner: CommandRunner - var habitId = -1L + var habitUUID: String? = null lateinit var habitType: HabitType var parentGroup: HabitGroup? = null var unit = "" @@ -98,10 +98,20 @@ class EditHabitActivity : AppCompatActivity() { binding = ActivityEditHabitBinding.inflate(layoutInflater) setContentView(binding.root) - if (intent.hasExtra("habitId")) { + if (intent.hasExtra("parentGroupUUID")) { + val parentGroupUUID = intent.getStringExtra("parentGroupUUID") + parentGroup = component.habitGroupList.getByUUID(parentGroupUUID) + } + + if (intent.hasExtra("habitUUID")) { binding.toolbar.title = getString(R.string.edit_habit) - habitId = intent.getLongExtra("habitId", -1) - val habit = component.habitList.getById(habitId)!! + habitUUID = intent.getStringExtra("habitUUID")!! + val habitList = if (parentGroup != null) { + parentGroup!!.habitList + } else { + component.habitList + } + val habit = habitList.getByUUID(habitUUID)!! habitType = habit.type color = habit.color freqNum = habit.frequency.numerator @@ -112,7 +122,6 @@ class EditHabitActivity : AppCompatActivity() { reminderMin = it.minute reminderDays = it.days } - parentGroup = habit.parent binding.nameInput.setText(habit.name) binding.questionInput.setText(habit.question) binding.notesInput.setText(habit.description) @@ -120,14 +129,10 @@ class EditHabitActivity : AppCompatActivity() { binding.targetInput.setText(habit.targetValue.toString()) } else { habitType = HabitType.fromInt(intent.getIntExtra("habitType", HabitType.YES_NO.value)) - if (intent.hasExtra("parentGroupUUID")) { - val parentGroupUUID = intent.getStringExtra("parentGroupUUID")!! - parentGroup = component.habitGroupList.getByUUID(parentGroupUUID) - } } if (state != null) { - habitId = state.getLong("habitId") + habitUUID = state.getString("habitUUID") habitType = HabitType.fromInt(state.getInt("habitType")) color = PaletteColor(state.getInt("paletteColor")) freqNum = state.getInt("freqNum") @@ -274,8 +279,8 @@ class EditHabitActivity : AppCompatActivity() { } var original: Habit? = null - if (habitId >= 0) { - original = habitList.getById(habitId)!! + if (habitUUID != null) { + original = habitList.getByUUID(habitUUID)!! habit.copyFrom(original) } @@ -300,10 +305,10 @@ class EditHabitActivity : AppCompatActivity() { habit.parentID = parentGroup?.id habit.parentUUID = parentGroup?.uuid - val command = if (habitId >= 0) { + val command = if (habitUUID != null) { EditHabitCommand( habitList, - habitId, + habitUUID!!, habit ) } else { @@ -382,7 +387,7 @@ class EditHabitActivity : AppCompatActivity() { override fun onSaveInstanceState(state: Bundle) { super.onSaveInstanceState(state) with(state) { - putLong("habitId", habitId) + putString("habitUUID", habitUUID) putInt("habitType", habitType.value) putInt("paletteColor", color.paletteIndex) putInt("androidColor", androidColor) 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 6aed4f7c1..f446cb041 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 @@ -100,8 +100,9 @@ class IntentFactory fun startEditActivity(context: Context, habit: Habit): Intent { val intent = startEditActivity(context) - intent.putExtra("habitId", habit.id) + intent.putExtra("habitUUID", habit.uuid) intent.putExtra("habitType", habit.type) + intent.putExtra("parentGroupUUID", habit.parentUUID) return intent } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/EditHabitCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/EditHabitCommand.kt index 8e168293d..733273961 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/EditHabitCommand.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/EditHabitCommand.kt @@ -24,11 +24,11 @@ import org.isoron.uhabits.core.models.HabitNotFoundException data class EditHabitCommand( val habitList: HabitList, - val habitId: Long, + val habitUUID: String, val modified: Habit ) : Command { override fun run() { - val habit = habitList.getById(habitId) ?: throw HabitNotFoundException() + val habit = habitList.getByUUID(habitUUID) ?: throw HabitNotFoundException() habit.copyFrom(modified) habitList.update(habit) habit.observable.notifyListeners() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt index 0c93c74ad..d5bd237a1 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt @@ -89,7 +89,7 @@ class LoopDBImporter val modified = modelFactory.buildHabit() habitRecord.id = habit.id habitRecord.copyTo(modified) - EditHabitCommand(habitList, habit.id!!, modified).run() + EditHabitCommand(habitList, habit.uuid!!, modified).run() } // Reload saved version of the habit From c1f0dae683e01f674c654040187ffa24135b5ca2 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sat, 6 Jul 2024 09:00:25 +0200 Subject: [PATCH 22/51] Implement habit group score ring, sorting sub habits (except with score) --- .../habits/list/views/HabitCardView.kt | 1 + .../core/commands/DeleteHabitsCommand.kt | 9 ++- .../commands/RefreshParentGroupCommand.kt | 34 +++++++++++ .../org/isoron/uhabits/core/models/Habit.kt | 4 -- .../screens/habits/list/HabitCardListCache.kt | 57 ++++++++++--------- .../screens/habits/list/ListHabitsBehavior.kt | 9 +++ .../list/ListHabitsSelectionMenuBehavior.kt | 2 +- 7 files changed, 83 insertions(+), 33 deletions(-) create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/RefreshParentGroupCommand.kt diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index e047549b9..c973c19be 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -97,6 +97,7 @@ class HabitCardView( set(value) { scoreRing.setPercentage(value.toFloat()) scoreRing.setPrecision(1.0f / 16) + behavior.onChangeScore(habit!!) } var unit diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitsCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitsCommand.kt index e61ba1626..fbbfafcd4 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitsCommand.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitsCommand.kt @@ -26,6 +26,13 @@ data class DeleteHabitsCommand( val selected: List ) : Command { override fun run() { - for (h in selected) habitList.remove(h) + for (h in selected) { + if (!h.isSubHabit()) { + habitList.remove(h) + } else { + val list = h.parent!!.habitList + list.remove(h) + } + } } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/RefreshParentGroupCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/RefreshParentGroupCommand.kt new file mode 100644 index 000000000..70322c3c0 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/RefreshParentGroupCommand.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package org.isoron.uhabits.core.commands + +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroupList + +data class RefreshParentGroupCommand( + val habit: Habit, + val habitGroupList: HabitGroupList +) : Command { + override fun run() { + if (!habit.isSubHabit()) return + val hgr = habit.parent + hgr!!.recompute() + habitGroupList.resort() + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index 2d41d0384..25b5320bb 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -113,10 +113,6 @@ data class Habit( return computedEntries.getKnown().lastOrNull()?.timestamp ?: DateUtils.getTodayWithOffset() } - fun isInGroup(): Boolean { - return (parentID != null) - } - fun copyFrom(other: Habit) { this.color = other.color this.description = other.description diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index 0bab9a99c..be2612b0c 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -214,49 +214,46 @@ class HabitCardListCache @Inject constructor( @Synchronized fun remove(uuid: String) { - val type = data.positionTypes[data.uuidToPosition[uuid]!!] + val position = data.uuidToPosition[uuid] ?: return + val type = data.positionTypes[position] if (type == STANDALONE_HABIT) { val h = data.uuidToHabit[uuid] if (h != null) { - val position = data.habits.indexOf(h) - data.habits.removeAt(position) - data.checkmarks.remove(uuid) - data.notes.remove(uuid) - data.scores.remove(uuid) - data.decrementPositions(position + 1, data.positionTypes.size) - listener.onItemRemoved(position) + val pos = data.habits.indexOf(h) + data.habits.removeAt(pos) + data.removeWithUUID(uuid) + data.positionTypes.removeAt(pos) + data.decrementPositions(pos + 1, data.positionTypes.size) + listener.onItemRemoved(pos) } } else if (type == SUB_HABIT) { val h = data.uuidToHabit[uuid] if (h != null) { - val position = data.uuidToPosition[uuid]!! + val pos = data.uuidToPosition[uuid]!! val hgrUUID = h.parentUUID val hgr = data.uuidToHabitGroup[hgrUUID] val hgrIdx = data.habitGroups.indexOf(hgr) data.subHabits[hgrIdx].remove(h) - data.checkmarks.remove(uuid) - data.notes.remove(uuid) - data.scores.remove(uuid) - data.decrementPositions(position + 1, data.positionTypes.size) - listener.onItemRemoved(position) + data.removeWithUUID(uuid) + data.positionTypes.removeAt(pos) + data.decrementPositions(pos + 1, data.positionTypes.size) + listener.onItemRemoved(pos) } } else if (type == HABIT_GROUP) { val hgr = data.uuidToHabitGroup[uuid] if (hgr != null) { - val position = data.uuidToPosition[uuid]!! + val pos = data.uuidToPosition[uuid]!! val hgrIdx = data.habitGroups.indexOf(hgr) for (habit in data.subHabits[hgrIdx].reversed()) { - data.checkmarks.remove(habit.uuid) - data.notes.remove(habit.uuid) - data.scores.remove(habit.uuid) + data.removeWithUUID(habit.uuid) listener.onItemRemoved(data.uuidToPosition[habit.uuid]!!) } data.subHabits.removeAt(hgrIdx) data.habitGroups.removeAt(hgrIdx) - data.scores.remove(hgr.uuid) + data.removeWithUUID(hgr.uuid) data.rebuildPositions() - listener.onItemRemoved(position) + listener.onItemRemoved(pos) } } } @@ -466,7 +463,6 @@ class HabitCardListCache @Inject constructor( @Synchronized fun decrementPositions(fromPosition: Int, toPosition: Int) { - positionTypes.removeAt(fromPosition) for (pos in positionToHabit.keys.sortedBy { it }) { if (pos in fromPosition..toPosition) { positionToHabit[pos - 1] = positionToHabit[pos]!! @@ -508,10 +504,11 @@ class HabitCardListCache @Inject constructor( if (type == STANDALONE_HABIT) { habits.removeAt(fromPosition) + positionTypes.removeAt(fromPosition) if (fromPosition < checkedToPosition) { decrementPositions(fromPosition + 1, checkedToPosition) } else { - incrementPositions(toPosition, fromPosition - 1) + incrementPositions(checkedToPosition, fromPosition - 1) } habits.add(checkedToPosition, habit) positionTypes.add(checkedToPosition, STANDALONE_HABIT) @@ -520,10 +517,11 @@ class HabitCardListCache @Inject constructor( val hgrIdx = habitGroups.indexOf(hgr) val h = positionToHabit[fromPosition]!! subHabits[hgrIdx].remove(h) + positionTypes.removeAt(fromPosition) if (fromPosition < checkedToPosition) { decrementPositions(fromPosition + 1, checkedToPosition) } else { - incrementPositions(toPosition, fromPosition - 1) + incrementPositions(checkedToPosition, fromPosition - 1) } subHabits[hgrIdx].add(checkedToPosition - uuidToPosition[hgr!!.uuid]!! - 1, habit) positionTypes.add(checkedToPosition, SUB_HABIT) @@ -556,6 +554,15 @@ class HabitCardListCache @Inject constructor( listener.onItemMoved(fromPosition, toPosition) } + fun removeWithUUID(uuid: String?) { + uuidToPosition.remove(uuid) + uuidToHabit.remove(uuid) + uuidToHabitGroup.remove(uuid) + scores.remove(uuid) + notes.remove(uuid) + checkmarks.remove(uuid) + } + /** * Creates a new CacheData without any content. */ @@ -654,10 +661,6 @@ class HabitCardListCache @Inject constructor( data.habits.add(position, habit) data.positionTypes.add(position, STANDALONE_HABIT) } else { -// val parent = data.uuidToHabitGroup[habit.parentUUID] -// val parentIdx = data.habitGroups.indexOf(parent) -// val parentPosition = data.uuidToPosition[habit.parentUUID]!! -// data.subHabits[parentIdx].add(position - parentPosition - 1, habit) data.positionTypes.add(position, SUB_HABIT) } data.incrementPositions(position, data.positionTypes.size - 1) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index 73ac30d23..4e35841e9 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -20,9 +20,11 @@ package org.isoron.uhabits.core.ui.screens.habits.list import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateRepetitionCommand +import org.isoron.uhabits.core.commands.RefreshParentGroupCommand import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitGroup +import org.isoron.uhabits.core.models.HabitGroupList import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitType import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST @@ -41,6 +43,7 @@ import kotlin.math.roundToInt open class ListHabitsBehavior @Inject constructor( private val habitList: HabitList, + private val habitGroupList: HabitGroupList, private val dirFinder: DirFinder, private val taskRunner: TaskRunner, private val screen: Screen, @@ -141,6 +144,12 @@ open class ListHabitsBehavior @Inject constructor( if (value == YES_MANUAL) screen.showConfetti(habit.color, x, y) } + fun onChangeScore(habit: Habit) { + commandRunner.run( + RefreshParentGroupCommand(habit, habitGroupList) + ) + } + enum class Message { COULD_NOT_EXPORT, IMPORT_SUCCESSFUL, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt index 231fa4c45..1608b757f 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt @@ -95,8 +95,8 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( { adapter.performRemove(adapter.getSelectedHabits()) adapter.performRemoveHabitGroup(adapter.getSelectedHabitGroups()) - commandRunner.run(DeleteHabitsCommand(habitList, adapter.getSelectedHabits())) commandRunner.run(DeleteHabitGroupsCommand(habitGroupList, adapter.getSelectedHabitGroups())) + commandRunner.run(DeleteHabitsCommand(habitList, adapter.getSelectedHabits())) adapter.clearSelection() }, adapter.getSelectedHabits().size + adapter.getSelectedHabitGroups().size From c0602498062de3e2808d5323e24b9b5f6915ed8b Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sat, 6 Jul 2024 11:29:55 +0200 Subject: [PATCH 23/51] Fixed sorting habit groups and sub habits by score --- .../isoron/uhabits/core/models/HabitList.kt | 11 +++++++++++ .../core/models/memory/MemoryHabitList.kt | 7 +++++++ .../core/models/sqlite/SQLiteHabitList.kt | 6 ++++++ .../screens/habits/list/HabitCardListCache.kt | 19 ++++++------------- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt index 54b19dac1..2309260e2 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt @@ -136,6 +136,17 @@ abstract class HabitList : Iterable { */ abstract fun remove(h: Habit) + /** + * Removes the reference to the habit from the list at the given position. + * + * Does not affect the repository or records + * + * If the given habit is not in the list, does nothing. + * + * @param h the habit to be removed. + */ + abstract fun removeAt(position: Int) + /** * Removes all the habits from the list. */ diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt index c17a113fb..15e6f7ef0 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt @@ -191,6 +191,13 @@ class MemoryHabitList : HabitList { observable.notifyListeners() } + @Synchronized + override fun removeAt(position: Int) { + throwIfHasParent() + list.removeAt(position) + observable.notifyListeners() + } + @Synchronized override fun reorder(from: Habit, to: Habit) { throwIfHasParent() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt index c53144a90..0988ebfda 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt @@ -157,6 +157,12 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory observable.notifyListeners() } + @Synchronized + override fun removeAt(position: Int) { + loadRecords() + list.removeAt(position) + } + @Synchronized override fun removeAll() { list.removeAll() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index be2612b0c..489e95df4 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -261,11 +261,6 @@ class HabitCardListCache @Inject constructor( @Synchronized fun reorder(from: Int, to: Int) { if (from == to) return - val uuid = if (data.positionTypes[from] == STANDALONE_HABIT) { - data.positionToHabit[from]!!.uuid - } else { - data.positionToHabitGroup[from]!!.uuid - } if (data.positionTypes[from] == STANDALONE_HABIT) { val habit = data.positionToHabit[from]!! data.performMove(habit, from, to) @@ -423,10 +418,7 @@ class HabitCardListCache @Inject constructor( if (habit.parentUUID == null) { return position <= habits.size } else { - val parent = uuidToHabitGroup[habit.parentUUID] - if (parent == null) { - return false - } + val parent = uuidToHabitGroup[habit.parentUUID] ?: return false val parentPosition = uuidToPosition[habit.parentUUID]!! val parentIndex = habitGroups.indexOf(parent) val nextGroup = habitGroups.getOrNull(parentIndex + 1) @@ -494,7 +486,7 @@ class HabitCardListCache @Inject constructor( // Workaround for https://github.com/iSoron/uhabits/issues/968 val checkedToPosition = if (toPosition > positionTypes.size) { logger.error("performMove: $toPosition for habit is strictly higher than ${habits.size}") - positionTypes.size + positionTypes.size - 1 } else { toPosition } @@ -515,15 +507,16 @@ class HabitCardListCache @Inject constructor( } else { val hgr = uuidToHabitGroup[habit.parentUUID] val hgrIdx = habitGroups.indexOf(hgr) - val h = positionToHabit[fromPosition]!! - subHabits[hgrIdx].remove(h) + val fromIdx = subHabits[hgrIdx].indexOf(habit) + subHabits[hgrIdx].removeAt(fromIdx) positionTypes.removeAt(fromPosition) if (fromPosition < checkedToPosition) { decrementPositions(fromPosition + 1, checkedToPosition) } else { incrementPositions(checkedToPosition, fromPosition - 1) } - subHabits[hgrIdx].add(checkedToPosition - uuidToPosition[hgr!!.uuid]!! - 1, habit) + val toIdx = checkedToPosition - uuidToPosition[hgr!!.uuid]!! - 1 + subHabits[hgrIdx].add(toIdx, habit) positionTypes.add(checkedToPosition, SUB_HABIT) } From c77da1803f07584c1ab4688eab974252bc06a49d Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sat, 6 Jul 2024 21:21:04 +0200 Subject: [PATCH 24/51] Implement saving of sub habits but breaks updating group score ring --- .../habits/list/views/HabitCardView.kt | 2 +- .../core/commands/DeleteHabitsCommand.kt | 3 ++- .../isoron/uhabits/core/models/HabitGroup.kt | 4 ++-- .../isoron/uhabits/core/models/HabitList.kt | 2 +- .../models/sqlite/SQLiteHabitGroupList.kt | 1 + .../core/models/sqlite/SQLiteHabitList.kt | 24 +++---------------- .../models/sqlite/records/HabitGroupRecord.kt | 1 + .../screens/habits/list/HabitCardListCache.kt | 24 +++++++++++-------- .../src/jvmMain/resources/migrations/26.sql | 10 ++++---- 9 files changed, 29 insertions(+), 42 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index c973c19be..649829b3a 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -97,7 +97,7 @@ class HabitCardView( set(value) { scoreRing.setPercentage(value.toFloat()) scoreRing.setPrecision(1.0f / 16) - behavior.onChangeScore(habit!!) +// behavior.onChangeScore(habit!!) } var unit diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitsCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitsCommand.kt index fbbfafcd4..25a41feab 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitsCommand.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitsCommand.kt @@ -19,6 +19,7 @@ package org.isoron.uhabits.core.commands import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitGroup import org.isoron.uhabits.core.models.HabitList data class DeleteHabitsCommand( @@ -30,7 +31,7 @@ data class DeleteHabitsCommand( if (!h.isSubHabit()) { habitList.remove(h) } else { - val list = h.parent!!.habitList + val list = (h.parent as HabitGroup).habitList list.remove(h) } } 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 0e3293493..629c76b8c 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 @@ -19,7 +19,7 @@ data class HabitGroup( ) { init { if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "") - habitList.groupUUID = this.uuid +// habitList.groupID = this.id } var observable = ModelObservable() @@ -78,7 +78,7 @@ data class HabitGroup( this.question = other.question this.reminder = other.reminder this.uuid = other.uuid - this.habitList.groupUUID = this.uuid + this.habitList.groupID = this.id } override fun equals(other: Any?): Boolean { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt index 2309260e2..b3b033903 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitList.kt @@ -34,7 +34,7 @@ abstract class HabitList : Iterable { @JvmField protected val filter: HabitMatcher - var groupUUID: String? = null + var groupID: Long? = null /** * Creates a new HabitList. diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt index f9a44da65..6fd62d7cb 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt @@ -40,6 +40,7 @@ class SQLiteHabitGroupList @Inject constructor(private val modelFactory: ModelFa record.copyFrom(habitGroup) repository.save(record) habitGroup.id = record.id + habitGroup.habitList.groupID = record.id list.add(habitGroup) observable.notifyListeners() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt index 0988ebfda..76f061a5a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitList.kt @@ -37,18 +37,15 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory private fun loadRecords() { if (loaded) return loaded = true - list.groupUUID = this.groupUUID + list.groupID = this.groupID list.removeAll() val records = repository.findAll("order by position") - var shouldRebuildOrder = false - for ((expectedPosition, rec) in records.withIndex()) { - if (rec.position != expectedPosition) shouldRebuildOrder = true + for (rec in records) { val h = modelFactory.buildHabit() rec.copyTo(h) (h.originalEntries as SQLiteEntryList).habitId = h.id - if (h.parentUUID == list.groupUUID) list.add(h) + if (h.parentID == list.groupID) list.add(h) } - if (shouldRebuildOrder) rebuildOrder() } @Synchronized @@ -129,19 +126,6 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory return list.iterator() } - @Synchronized - private fun rebuildOrder() { - val records = repository.findAll("order by position") - repository.executeAsTransaction { - for ((pos, r) in records.withIndex()) { - if (r.position != pos) { - r.position = pos - repository.save(r) - } - } - } - } - @Synchronized override fun remove(h: Habit) { loadRecords() @@ -153,7 +137,6 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory h.originalEntries.clear() repository.remove(record) } - rebuildOrder() observable.notifyListeners() } @@ -206,7 +189,6 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory @Synchronized override fun repair() { loadRecords() - rebuildOrder() 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 0b34be917..9f769644e 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 @@ -79,6 +79,7 @@ class HabitGroupRecord { habitGroup.isArchived = archived != 0 habitGroup.position = position!! habitGroup.uuid = uuid + habitGroup.habitList.groupID = id if (reminderHour != null && reminderMin != null) { habitGroup.reminder = Reminder( reminderHour!!, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index 489e95df4..173ed700c 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -146,7 +146,7 @@ class HabitCardListCache @Inject constructor( @get:Synchronized val subHabitCount: Int - get() = data.subHabits.sumOf { it.size() } + get() = data.subHabits.sumOf { it.size } @get:Synchronized @set:Synchronized @@ -279,8 +279,9 @@ class HabitCardListCache @Inject constructor( fun setFilter(matcher: HabitMatcher) { filteredHabits = habits.getFiltered(matcher) filteredHabitGroups = habitGroups.getFiltered(matcher) - for (idx in filteredSubHabits.indices) { - filteredSubHabits[idx] = filteredSubHabits[idx].getFiltered(matcher) + filteredSubHabits.clear() + for (hgr in filteredHabitGroups) { + filteredSubHabits.add(hgr.habitList.getFiltered(matcher)) } } @@ -306,7 +307,7 @@ class HabitCardListCache @Inject constructor( val uuidToHabitGroup: HashMap = HashMap() val habits: MutableList val habitGroups: MutableList - val subHabits: MutableList + val subHabits: MutableList> val uuidToPosition: HashMap val positionTypes: MutableList val positionToHabit: HashMap @@ -368,15 +369,14 @@ class HabitCardListCache @Inject constructor( habits.add(h) } - for (hgr in filteredHabitGroups) { + for ((index, hgr) in filteredHabitGroups.withIndex()) { if (hgr.uuid == null) continue habitGroups.add(hgr) - val habitList = hgr.habitList - subHabits.add(habitList) - - for (h in habitList) { - if (h.uuid == null) continue + val habitList = LinkedList() + for (h in filteredSubHabits[index]) { + habitList.add(h) } + subHabits.add(habitList) } } @@ -654,6 +654,10 @@ class HabitCardListCache @Inject constructor( data.habits.add(position, habit) data.positionTypes.add(position, STANDALONE_HABIT) } else { + val hgr = data.uuidToHabitGroup[habit.parentUUID] + val hgrIdx = data.habitGroups.indexOf(hgr) + val habitIndex = newData.subHabits[hgrIdx].indexOf(habit) + data.subHabits[hgrIdx].add(habitIndex, habit) data.positionTypes.add(position, SUB_HABIT) } data.incrementPositions(position, data.positionTypes.size - 1) diff --git a/uhabits-core/src/jvmMain/resources/migrations/26.sql b/uhabits-core/src/jvmMain/resources/migrations/26.sql index 067a6bb1f..8d0c0f7df 100644 --- a/uhabits-core/src/jvmMain/resources/migrations/26.sql +++ b/uhabits-core/src/jvmMain/resources/migrations/26.sql @@ -1,8 +1,3 @@ -alter table Habits add column skip_days integer not null default 0; -alter table Habits add column skip_days_list integer not null default 0; -alter table Habits add column parent_id integer; -alter table Habits add column parent_uuid text; - create table HabitGroups ( id integer primary key autoincrement, archived integer, @@ -16,4 +11,7 @@ create table HabitGroups ( reminder_min integer, question text not null default "", uuid text -); \ No newline at end of file +); + +alter table Habits add column parent_uuid text references habitgroups(uuid); +alter table Habits add column parent_id integer references habitgroups(id); \ No newline at end of file From be81a06e62579cc05976fd59803b151623c21e95 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sun, 7 Jul 2024 00:05:04 +0200 Subject: [PATCH 25/51] Sub habits and groups are filtered correctly --- .../org/isoron/uhabits/HabitsApplication.kt | 2 +- .../isoron/uhabits/core/models/HabitGroup.kt | 20 +++++++++++++- .../uhabits/core/models/HabitGroupList.kt | 27 +++---------------- .../models/memory/MemoryHabitGroupList.kt | 18 ++++++++++--- .../models/sqlite/SQLiteHabitGroupList.kt | 4 +++ .../screens/habits/list/HabitCardListCache.kt | 19 +++---------- .../screens/habits/list/ListHabitsBehavior.kt | 8 +++--- 7 files changed, 51 insertions(+), 47 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt index da242e3d2..b8421c5c1 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt @@ -81,7 +81,7 @@ class HabitsApplication : Application() { val habitGroupList = component.habitGroupList for (hgr in habitGroupList) hgr.recompute() - habitGroupList.populateGroupsWith(habitList) + habitGroupList.attachHabitsToGroups() widgetUpdater = component.widgetUpdater.apply { startListening() 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 629c76b8c..83f02dcc5 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 @@ -17,9 +17,27 @@ data class HabitGroup( val scores: ScoreList, val streaks: StreakList ) { + + constructor( + parent: HabitGroup, + matcher: HabitMatcher + ) : this( + parent.color, + parent.description, + parent.id, + parent.isArchived, + parent.name, + parent.position, + parent.question, + parent.reminder, + parent.uuid, + parent.habitList.getFiltered(matcher), + parent.scores, + parent.streaks + ) + init { if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "") -// habitList.groupID = this.id } var observable = ModelObservable() 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 471e0b62f..376deb81e 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 @@ -166,31 +166,10 @@ abstract class HabitGroupList : Iterable { } /** - * For an empty Habit group list, and a given list of habits, - * populate the habit groups with their appropriate habits - * - * @param habitList list of habits to add to the groups + * For each habit group, point all the habits in it + * to the group it is contained in * */ - fun populateGroupsWith(habitList: HabitList) { - val toRemove = mutableListOf() - for (habit in habitList) { - val hgr = getByUUID(habit.parentUUID) - if (hgr != null) { - hgr.habitList.add(habit) - habit.parent = hgr - toRemove.add(habit.uuid) - } - } - for (uuid in toRemove) { - val h = habitList.getByUUID(uuid) - if (h != null) { - habitList.remove(h) - } - } - for (hgr in this) { - hgr.recompute() - } - } + abstract fun attachHabitsToGroups() /** * Writes the list of habit groups to the given writer, in CSV format. There is 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 983514a4b..732119513 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 @@ -45,6 +45,9 @@ class MemoryHabitGroupList : HabitGroupList { primaryOrder = parent.primaryOrder secondaryOrder = parent.secondaryOrder parent.observable.addListener { loadFromParent() } + for (hgr in parent.list) { + hgr.habitList.observable.addListener { loadFromParent() } + } loadFromParent() } @@ -184,6 +187,14 @@ class MemoryHabitGroupList : HabitGroupList { resort() } + override fun attachHabitsToGroups() { + for (hgr in list) { + for (h in hgr.habitList) { + h.parent = hgr + } + } + } + private fun throwIfHasParent() { check(parent == null) { "Filtered lists cannot be modified directly. " + @@ -195,9 +206,10 @@ class MemoryHabitGroupList : HabitGroupList { private fun loadFromParent() { checkNotNull(parent) list.clear() - for (h in parent!!) { - if (filter.matches(h)) { - list.add(h) + for (hgr in parent!!) { + if (filter.matches(hgr)) { + val filteredHgr = HabitGroup(hgr, filter) + list.add(filteredHgr) } } resort() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt index 6fd62d7cb..4ec45595f 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteHabitGroupList.kt @@ -189,6 +189,10 @@ class SQLiteHabitGroupList @Inject constructor(private val modelFactory: ModelFa observable.notifyListeners() } + override fun attachHabitsToGroups() { + list.attachHabitsToGroups() + } + override fun resort() { list.resort() observable.notifyListeners() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index 173ed700c..a5c5100bd 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -68,7 +68,6 @@ class HabitCardListCache @Inject constructor( private val data: CacheData private var filteredHabits: HabitList private var filteredHabitGroups: HabitGroupList - private var filteredSubHabits: MutableList private val taskRunner: TaskRunner @Synchronized @@ -157,7 +156,6 @@ class HabitCardListCache @Inject constructor( habitGroups.primaryOrder = order filteredHabits.primaryOrder = order filteredHabitGroups.primaryOrder = order - filteredSubHabits.forEach { it.primaryOrder = order } refreshAllHabits() } @@ -170,7 +168,6 @@ class HabitCardListCache @Inject constructor( habitGroups.secondaryOrder = order filteredHabits.secondaryOrder = order filteredHabitGroups.secondaryOrder = order - filteredSubHabits.forEach { it.secondaryOrder = order } refreshAllHabits() } @@ -246,8 +243,9 @@ class HabitCardListCache @Inject constructor( val hgrIdx = data.habitGroups.indexOf(hgr) for (habit in data.subHabits[hgrIdx].reversed()) { + val habitPos = data.uuidToPosition[habit.uuid]!! data.removeWithUUID(habit.uuid) - listener.onItemRemoved(data.uuidToPosition[habit.uuid]!!) + listener.onItemRemoved(habitPos) } data.subHabits.removeAt(hgrIdx) data.habitGroups.removeAt(hgrIdx) @@ -279,10 +277,6 @@ class HabitCardListCache @Inject constructor( fun setFilter(matcher: HabitMatcher) { filteredHabits = habits.getFiltered(matcher) filteredHabitGroups = habitGroups.getFiltered(matcher) - filteredSubHabits.clear() - for (hgr in filteredHabitGroups) { - filteredSubHabits.add(hgr.habitList.getFiltered(matcher)) - } } @Synchronized @@ -369,11 +363,11 @@ class HabitCardListCache @Inject constructor( habits.add(h) } - for ((index, hgr) in filteredHabitGroups.withIndex()) { + for (hgr in filteredHabitGroups) { if (hgr.uuid == null) continue habitGroups.add(hgr) val habitList = LinkedList() - for (h in filteredSubHabits[index]) { + for (h in hgr.habitList) { habitList.add(h) } subHabits.add(habitList) @@ -780,11 +774,6 @@ class HabitCardListCache @Inject constructor( init { filteredHabits = habits filteredHabitGroups = habitGroups - filteredSubHabits = LinkedList() - for (hgr in habitGroups) { - val subList = hgr.habitList - filteredSubHabits.add(subList) - } this.taskRunner = taskRunner listener = object : Listener {} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index 4e35841e9..7690d4e12 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -60,6 +60,7 @@ open class ListHabitsBehavior @Inject constructor( } fun onEdit(habit: Habit, timestamp: Timestamp?) { + val list = if (habit.isSubHabit()) habit.parent!!.habitList else habitList val entry = habit.computedEntries.get(timestamp!!) if (habit.type == HabitType.NUMERICAL) { val oldValue = entry.value.toDouble() / 1000 @@ -73,7 +74,7 @@ open class ListHabitsBehavior @Inject constructor( screen.showConfetti(habit.color, x, y) } } - commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes)) + commandRunner.run(CreateRepetitionCommand(list, habit, timestamp, value, newNotes)) } } else { screen.showCheckmarkPopup( @@ -82,7 +83,7 @@ open class ListHabitsBehavior @Inject constructor( habit.color ) { newValue: Int, newNotes: String, x: Float, y: Float -> if (newValue != entry.value && newValue == YES_MANUAL) screen.showConfetti(habit.color, x, y) - commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes)) + commandRunner.run(CreateRepetitionCommand(list, habit, timestamp, newValue, newNotes)) } } } @@ -138,8 +139,9 @@ open class ListHabitsBehavior @Inject constructor( } fun onToggle(habit: Habit, timestamp: Timestamp, value: Int, notes: String, x: Float, y: Float) { + val list = if (habit.isSubHabit()) habit.parent!!.habitList else habitList commandRunner.run( - CreateRepetitionCommand(habitList, habit, timestamp, value, notes) + CreateRepetitionCommand(list, habit, timestamp, value, notes) ) if (value == YES_MANUAL) screen.showConfetti(habit.color, x, y) } From 8df1fb765c7a2d2d045e51eb22ef2e10269d5ace Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sun, 7 Jul 2024 00:29:58 +0200 Subject: [PATCH 26/51] Implement sub habit reordering within group --- .../core/ui/screens/habits/list/HabitCardListCache.kt | 5 +++-- .../core/ui/screens/habits/list/ListHabitsBehavior.kt | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index a5c5100bd..981cf0917 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -259,10 +259,11 @@ class HabitCardListCache @Inject constructor( @Synchronized fun reorder(from: Int, to: Int) { if (from == to) return - if (data.positionTypes[from] == STANDALONE_HABIT) { + val type = data.positionTypes[from] + if (type == STANDALONE_HABIT || type == SUB_HABIT) { val habit = data.positionToHabit[from]!! data.performMove(habit, from, to) - } else if (data.positionTypes[from] == HABIT_GROUP) { + } else { val habitGroup = data.positionToHabitGroup[from]!! data.performMove(habitGroup, from, to) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index 7690d4e12..ead0d43ef 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -112,7 +112,10 @@ open class ListHabitsBehavior @Inject constructor( } fun onReorderHabit(from: Habit, to: Habit) { - taskRunner.execute { habitList.reorder(from, to) } + if (from.parent == to.parent) { + val list = from.parent?.habitList ?: habitList + taskRunner.execute { list.reorder(from, to) } + } } fun onRepairDB() { From 26260fbbc4f36e303fa87aab7780073dc7e66910 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sun, 7 Jul 2024 10:26:28 +0200 Subject: [PATCH 27/51] Implement partial reordering or habit groups --- .../list/views/HabitCardListController.kt | 18 +++++++++++++----- .../screens/habits/list/ListHabitsBehavior.kt | 4 ++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListController.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListController.kt index 9ae4243c6..2d20557a3 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListController.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListController.kt @@ -50,12 +50,20 @@ class HabitCardListController @Inject constructor( if (from == to) return cancelSelection() - val habitFrom = adapter.getItem(from) - val habitTo = adapter.getItem(to) - if (habitFrom == null || habitTo == null) return + val habitFrom = adapter.getHabit(from) + val habitTo = adapter.getHabit(to) + if (habitFrom != null && habitTo != null) { + adapter.performReorder(from, to) + behavior.onReorderHabit(habitFrom, habitTo) + return + } - adapter.performReorder(from, to) - behavior.onReorderHabit(habitFrom, habitTo) + val hgrFrom = adapter.getHabitGroup(from) + val hgrTo = adapter.getHabitGroup(to) + if (hgrFrom != null && hgrTo != null) { + adapter.performReorder(from, to) + behavior.onReorderHabitGroup(hgrFrom, hgrTo) + } } override fun onItemClick(position: Int) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index ead0d43ef..e80b37647 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -118,6 +118,10 @@ open class ListHabitsBehavior @Inject constructor( } } + fun onReorderHabitGroup(from: HabitGroup, to: HabitGroup) { + taskRunner.execute { habitGroupList.reorder(from, to) } + } + fun onRepairDB() { taskRunner.execute { habitList.repair() From 584aace548ef225813dfdf34b3a7e6f16f6da10f Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sun, 7 Jul 2024 12:08:06 +0200 Subject: [PATCH 28/51] Fix filtering by status --- .../screens/habits/list/HabitCardListCache.kt | 30 ++++++++++++++----- .../screens/habits/list/ListHabitsBehavior.kt | 5 ++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index 981cf0917..2be65e4b0 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -220,6 +220,8 @@ class HabitCardListCache @Inject constructor( data.habits.removeAt(pos) data.removeWithUUID(uuid) data.positionTypes.removeAt(pos) + data.positionIndices.removeAt(pos) + data.positionToHabit.remove(pos) data.decrementPositions(pos + 1, data.positionTypes.size) listener.onItemRemoved(pos) } @@ -233,6 +235,8 @@ class HabitCardListCache @Inject constructor( data.subHabits[hgrIdx].remove(h) data.removeWithUUID(uuid) data.positionTypes.removeAt(pos) + data.positionIndices.removeAt(pos) + data.positionToHabit.remove(pos) data.decrementPositions(pos + 1, data.positionTypes.size) listener.onItemRemoved(pos) } @@ -305,6 +309,7 @@ class HabitCardListCache @Inject constructor( val subHabits: MutableList> val uuidToPosition: HashMap val positionTypes: MutableList + val positionIndices: MutableList val positionToHabit: HashMap val positionToHabitGroup: HashMap val checkmarks: HashMap @@ -381,12 +386,14 @@ class HabitCardListCache @Inject constructor( positionToHabitGroup.clear() uuidToPosition.clear() positionTypes.clear() + positionIndices.clear() var position = 0 - for (h in habits) { + for ((idx, h) in habits.withIndex()) { uuidToHabit[h.uuid] = h uuidToPosition[h.uuid] = position positionToHabit[position] = h positionTypes.add(STANDALONE_HABIT) + positionIndices.add(idx) position++ } @@ -395,14 +402,16 @@ class HabitCardListCache @Inject constructor( uuidToPosition[hgr.uuid] = position positionToHabitGroup[position] = hgr positionTypes.add(HABIT_GROUP) + positionIndices.add(idx) val habitList = subHabits[idx] position++ - for (h in habitList) { + for ((hIdx, h) in habitList.withIndex()) { uuidToHabit[h.uuid] = h uuidToPosition[h.uuid] = position positionToHabit[position] = h positionTypes.add(SUB_HABIT) + positionIndices.add(hIdx) position++ } } @@ -429,13 +438,13 @@ class HabitCardListCache @Inject constructor( @Synchronized fun incrementPositions(from: Int, to: Int) { - for (pos in positionToHabit.keys.sortedByDescending { it }) { + for (pos in positionToHabit.keys.sortedDescending()) { if (pos in from..to) { positionToHabit[pos + 1] = positionToHabit[pos]!! positionToHabit.remove(pos) } } - for (pos in positionToHabitGroup.keys.sortedByDescending { it }) { + for (pos in positionToHabitGroup.keys.sortedDescending()) { if (pos in from..to) { positionToHabitGroup[pos + 1] = positionToHabitGroup[pos]!! positionToHabitGroup.remove(pos) @@ -450,13 +459,13 @@ class HabitCardListCache @Inject constructor( @Synchronized fun decrementPositions(fromPosition: Int, toPosition: Int) { - for (pos in positionToHabit.keys.sortedBy { it }) { + for (pos in positionToHabit.keys.sorted()) { if (pos in fromPosition..toPosition) { positionToHabit[pos - 1] = positionToHabit[pos]!! positionToHabit.remove(pos) } } - for (pos in positionToHabitGroup.keys.sortedBy { it }) { + for (pos in positionToHabitGroup.keys.sorted()) { if (pos in fromPosition..toPosition) { positionToHabitGroup[pos - 1] = positionToHabitGroup[pos]!! positionToHabitGroup.remove(pos) @@ -492,6 +501,7 @@ class HabitCardListCache @Inject constructor( if (type == STANDALONE_HABIT) { habits.removeAt(fromPosition) positionTypes.removeAt(fromPosition) + positionIndices.removeAt(fromPosition) if (fromPosition < checkedToPosition) { decrementPositions(fromPosition + 1, checkedToPosition) } else { @@ -499,12 +509,14 @@ class HabitCardListCache @Inject constructor( } habits.add(checkedToPosition, habit) positionTypes.add(checkedToPosition, STANDALONE_HABIT) + positionIndices.add(checkedToPosition, checkedToPosition) } else { val hgr = uuidToHabitGroup[habit.parentUUID] val hgrIdx = habitGroups.indexOf(hgr) val fromIdx = subHabits[hgrIdx].indexOf(habit) subHabits[hgrIdx].removeAt(fromIdx) positionTypes.removeAt(fromPosition) + positionIndices.removeAt(fromPosition) if (fromPosition < checkedToPosition) { decrementPositions(fromPosition + 1, checkedToPosition) } else { @@ -513,6 +525,7 @@ class HabitCardListCache @Inject constructor( val toIdx = checkedToPosition - uuidToPosition[hgr!!.uuid]!! - 1 subHabits[hgrIdx].add(toIdx, habit) positionTypes.add(checkedToPosition, SUB_HABIT) + positionIndices.add(checkedToPosition, toIdx) } positionToHabit[checkedToPosition] = habit @@ -528,7 +541,7 @@ class HabitCardListCache @Inject constructor( ) { if (positionTypes[fromPosition] != HABIT_GROUP) return if (!isValidInsert(habitGroup, toPosition)) return - val fromIdx = habitGroups.indexOf(habitGroup) + val fromIdx = positionIndices[fromPosition] val habitList = subHabits[fromIdx] val toIdx = habitGroups.indexOf(positionToHabitGroup[toPosition]) - (if (fromPosition < toPosition) 1 else 0) @@ -559,6 +572,7 @@ class HabitCardListCache @Inject constructor( habitGroups = LinkedList() subHabits = LinkedList() positionTypes = LinkedList() + positionIndices = LinkedList() uuidToPosition = HashMap() positionToHabit = HashMap() positionToHabitGroup = HashMap() @@ -648,12 +662,14 @@ class HabitCardListCache @Inject constructor( if (habit.parentUUID == null) { data.habits.add(position, habit) data.positionTypes.add(position, STANDALONE_HABIT) + data.positionIndices.add(position, position) } else { val hgr = data.uuidToHabitGroup[habit.parentUUID] val hgrIdx = data.habitGroups.indexOf(hgr) val habitIndex = newData.subHabits[hgrIdx].indexOf(habit) data.subHabits[hgrIdx].add(habitIndex, habit) data.positionTypes.add(position, SUB_HABIT) + data.positionIndices.add(position, habitIndex) } data.incrementPositions(position, data.positionTypes.size - 1) data.positionToHabit[position] = habit diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index e80b37647..ca3e9873a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -75,6 +75,7 @@ open class ListHabitsBehavior @Inject constructor( } } commandRunner.run(CreateRepetitionCommand(list, habit, timestamp, value, newNotes)) + commandRunner.run(RefreshParentGroupCommand(habit, habitGroupList)) } } else { screen.showCheckmarkPopup( @@ -84,6 +85,7 @@ open class ListHabitsBehavior @Inject constructor( ) { newValue: Int, newNotes: String, x: Float, y: Float -> if (newValue != entry.value && newValue == YES_MANUAL) screen.showConfetti(habit.color, x, y) commandRunner.run(CreateRepetitionCommand(list, habit, timestamp, newValue, newNotes)) + commandRunner.run(RefreshParentGroupCommand(habit, habitGroupList)) } } } @@ -150,6 +152,9 @@ open class ListHabitsBehavior @Inject constructor( commandRunner.run( CreateRepetitionCommand(list, habit, timestamp, value, notes) ) + commandRunner.run( + RefreshParentGroupCommand(habit, habitGroupList) + ) if (value == YES_MANUAL) screen.showConfetti(habit.color, x, y) } From 3ba214ff61664554502ec711ebd6d188ba827159 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sun, 7 Jul 2024 13:32:27 +0200 Subject: [PATCH 29/51] Fix streaks of HabitGroups --- .../isoron/uhabits/core/models/StreakList.kt | 19 +++--- .../screens/habits/list/HabitCardListCache.kt | 60 +++++++++---------- .../screens/habits/list/ListHabitsBehavior.kt | 6 -- 3 files changed, 42 insertions(+), 43 deletions(-) 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 d5df21f1a..76696e6ad 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 @@ -74,18 +74,23 @@ class StreakList { from: Timestamp, to: Timestamp ) { + list.clear() if (habitList.isEmpty) return var current = from var streakRunning = false var streakStart = from while (current <= to) { - if (habitList.all { it.streaks.isInStreaks(current) } && !streakRunning) { - streakStart = current - streakRunning = true - } else if (streakRunning) { - val streakEnd = current.minus(1) - list.add(Streak(streakStart, streakEnd)) - streakRunning = false + if (habitList.all { it.streaks.isInStreaks(current) }) { + if (!streakRunning) { + streakStart = current + streakRunning = true + } + } else { + if (streakRunning) { + val streakEnd = current.minus(1) + list.add(Streak(streakStart, streakEnd)) + streakRunning = false + } } current = current.plus(1) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index 2be65e4b0..ba730984d 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -216,35 +216,28 @@ class HabitCardListCache @Inject constructor( if (type == STANDALONE_HABIT) { val h = data.uuidToHabit[uuid] if (h != null) { - val pos = data.habits.indexOf(h) - data.habits.removeAt(pos) + data.habits.removeAt(position) data.removeWithUUID(uuid) - data.positionTypes.removeAt(pos) - data.positionIndices.removeAt(pos) - data.positionToHabit.remove(pos) - data.decrementPositions(pos + 1, data.positionTypes.size) - listener.onItemRemoved(pos) + data.removeWithPos(position) + data.decrementPositions(position + 1, data.positionTypes.size) + listener.onItemRemoved(position) } } else if (type == SUB_HABIT) { val h = data.uuidToHabit[uuid] if (h != null) { - val pos = data.uuidToPosition[uuid]!! val hgrUUID = h.parentUUID val hgr = data.uuidToHabitGroup[hgrUUID] val hgrIdx = data.habitGroups.indexOf(hgr) data.subHabits[hgrIdx].remove(h) data.removeWithUUID(uuid) - data.positionTypes.removeAt(pos) - data.positionIndices.removeAt(pos) - data.positionToHabit.remove(pos) - data.decrementPositions(pos + 1, data.positionTypes.size) - listener.onItemRemoved(pos) + data.removeWithPos(position) + data.decrementPositions(position + 1, data.positionTypes.size) + listener.onItemRemoved(position) } } else if (type == HABIT_GROUP) { val hgr = data.uuidToHabitGroup[uuid] if (hgr != null) { - val pos = data.uuidToPosition[uuid]!! - val hgrIdx = data.habitGroups.indexOf(hgr) + val hgrIdx = data.positionIndices[position] for (habit in data.subHabits[hgrIdx].reversed()) { val habitPos = data.uuidToPosition[habit.uuid]!! @@ -255,7 +248,7 @@ class HabitCardListCache @Inject constructor( data.habitGroups.removeAt(hgrIdx) data.removeWithUUID(hgr.uuid) data.rebuildPositions() - listener.onItemRemoved(pos) + listener.onItemRemoved(position) } } } @@ -500,8 +493,7 @@ class HabitCardListCache @Inject constructor( if (type == STANDALONE_HABIT) { habits.removeAt(fromPosition) - positionTypes.removeAt(fromPosition) - positionIndices.removeAt(fromPosition) + removeWithPos(fromPosition) if (fromPosition < checkedToPosition) { decrementPositions(fromPosition + 1, checkedToPosition) } else { @@ -513,10 +505,9 @@ class HabitCardListCache @Inject constructor( } else { val hgr = uuidToHabitGroup[habit.parentUUID] val hgrIdx = habitGroups.indexOf(hgr) - val fromIdx = subHabits[hgrIdx].indexOf(habit) + val fromIdx = positionIndices[fromPosition] subHabits[hgrIdx].removeAt(fromIdx) - positionTypes.removeAt(fromPosition) - positionIndices.removeAt(fromPosition) + removeWithPos(fromPosition) if (fromPosition < checkedToPosition) { decrementPositions(fromPosition + 1, checkedToPosition) } else { @@ -564,6 +555,12 @@ class HabitCardListCache @Inject constructor( checkmarks.remove(uuid) } + fun removeWithPos(pos: Int) { + positionTypes.removeAt(pos) + positionIndices.removeAt(pos) + positionToHabit.remove(pos) + } + /** * Creates a new CacheData without any content. */ @@ -664,9 +661,9 @@ class HabitCardListCache @Inject constructor( data.positionTypes.add(position, STANDALONE_HABIT) data.positionIndices.add(position, position) } else { - val hgr = data.uuidToHabitGroup[habit.parentUUID] - val hgrIdx = data.habitGroups.indexOf(hgr) - val habitIndex = newData.subHabits[hgrIdx].indexOf(habit) + val hgrPos = data.uuidToPosition[habit.parentUUID]!! + val hgrIdx = data.positionIndices[hgrPos] + val habitIndex = newData.positionIndices[position] data.subHabits[hgrIdx].add(habitIndex, habit) data.positionTypes.add(position, SUB_HABIT) data.positionIndices.add(position, habitIndex) @@ -685,10 +682,13 @@ class HabitCardListCache @Inject constructor( private fun performInsert(habitGroup: HabitGroup, position: Int) { if (!data.isValidInsert(habitGroup, position)) return val uuid = habitGroup.uuid - val prevIdx = newData.habitGroups.indexOf(habitGroup) + val prevIdx = newData.positionIndices[position] val habitList = newData.subHabits[prevIdx] - var idx = data.habitGroups.indexOf(data.positionToHabitGroup[position]) - if (idx < 0) idx = data.habitGroups.size + val idx = if (data.positionIndices.size > position) { + data.positionIndices[position] + } else { + data.habitGroups.size + } data.habitGroups.add(idx, habitGroup) data.subHabits.add(prevIdx, habitList) @@ -737,9 +737,9 @@ class HabitCardListCache @Inject constructor( val newPosition = if (type == STANDALONE_HABIT) { currentPosition } else { - val hgr = data.uuidToHabitGroup[habit.parentUUID] - val hgrIdx = data.habitGroups.indexOf(hgr) - newData.subHabits[hgrIdx].indexOf(habit) + data.uuidToPosition[hgr!!.uuid]!! + 1 + val hgrPos = data.uuidToPosition[habit.parentUUID]!! + val hgrIdx = data.positionIndices[hgrPos] + newData.subHabits[hgrIdx].indexOf(habit) + hgrPos + 1 } if (prevPosition < 0) { performInsert(habit, newPosition) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index ca3e9873a..03d33fc12 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -158,12 +158,6 @@ open class ListHabitsBehavior @Inject constructor( if (value == YES_MANUAL) screen.showConfetti(habit.color, x, y) } - fun onChangeScore(habit: Habit) { - commandRunner.run( - RefreshParentGroupCommand(habit, habitGroupList) - ) - } - enum class Message { COULD_NOT_EXPORT, IMPORT_SUCCESSFUL, From 6b1eed0bdc804ab1ef0fdbcfd4f78d1e3bb846f6 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sun, 7 Jul 2024 14:36:50 +0200 Subject: [PATCH 30/51] Fix updating of group score ring on appropriate action --- .../activities/habits/edit/EditHabitActivity.kt | 8 ++++++++ .../isoron/uhabits/core/models/HabitGroupList.kt | 14 ++++++++++++++ .../org/isoron/uhabits/core/models/ScoreList.kt | 4 +++- .../org/isoron/uhabits/core/models/StreakList.kt | 3 ++- .../habits/list/ListHabitsSelectionMenuBehavior.kt | 10 ++++++++++ .../uhabits/core/commands/EditHabitCommandTest.kt | 2 +- .../screens/habits/list/ListHabitsBehaviorTest.kt | 1 + 7 files changed, 39 insertions(+), 3 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt index 8c70d3cb5..c9fac1f4f 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt @@ -45,6 +45,7 @@ import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateHabitCommand import org.isoron.uhabits.core.commands.EditHabitCommand +import org.isoron.uhabits.core.commands.RefreshParentGroupCommand import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitGroup @@ -319,6 +320,13 @@ class EditHabitActivity : AppCompatActivity() { ) } component.commandRunner.run(command) + + if (habit.parentID != null) { + val habitGroupList = component.habitGroupList + val refreshCommand = RefreshParentGroupCommand(habit, habitGroupList) + component.commandRunner.run(refreshCommand) + } + finish() } 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 376deb81e..b1cc256ff 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,6 +78,20 @@ abstract class HabitGroupList : Iterable { return null } + /** + * Returns the habit with the specified UUID which is + * present in any of the habit groups within this habit group list. + */ + fun getHabitByID(id: Long): Habit? { + for (hgr in this) { + val habit = hgr.habitList.getById(id) + if (habit != null) { + return habit + } + } + return null + } + /** * Returns the habit group that occupies a certain position. * 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 89fff0dfc..fa076c046 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 @@ -145,7 +145,9 @@ class ScoreList { ) { var current = to while (current >= from) { - val habitScores = habitList.map { it.scores[current].value } + val habitScores = habitList + .filter { !it.isArchived } + .map { it.scores[current].value } val averageScore = if (habitScores.isNotEmpty()) habitScores.average() else 0.0 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 76696e6ad..2214213fc 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 @@ -79,8 +79,9 @@ class StreakList { var current = from var streakRunning = false var streakStart = from + val notArchivedHabits = habitList.filter { !it.isArchived } while (current <= to) { - if (habitList.all { it.streaks.isInStreaks(current) }) { + if (notArchivedHabits.all { it.streaks.isInStreaks(current) }) { if (!streakRunning) { streakStart = current streakRunning = true diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt index 1608b757f..178a193c7 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt @@ -25,6 +25,7 @@ import org.isoron.uhabits.core.commands.ChangeHabitGroupColorCommand import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.DeleteHabitGroupsCommand import org.isoron.uhabits.core.commands.DeleteHabitsCommand +import org.isoron.uhabits.core.commands.RefreshParentGroupCommand import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitGroup @@ -61,6 +62,9 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( fun onArchiveHabits() { commandRunner.run(ArchiveHabitsCommand(habitList, adapter.getSelectedHabits())) commandRunner.run(ArchiveHabitGroupsCommand(habitGroupList, adapter.getSelectedHabitGroups())) + for (habit in adapter.getSelectedHabits()) { + commandRunner.run(RefreshParentGroupCommand(habit, habitGroupList)) + } adapter.clearSelection() } @@ -97,6 +101,9 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( adapter.performRemoveHabitGroup(adapter.getSelectedHabitGroups()) commandRunner.run(DeleteHabitGroupsCommand(habitGroupList, adapter.getSelectedHabitGroups())) commandRunner.run(DeleteHabitsCommand(habitList, adapter.getSelectedHabits())) + for (habit in adapter.getSelectedHabits()) { + commandRunner.run(RefreshParentGroupCommand(habit, habitGroupList)) + } adapter.clearSelection() }, adapter.getSelectedHabits().size + adapter.getSelectedHabitGroups().size @@ -117,6 +124,9 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( fun onUnarchiveHabits() { commandRunner.run(UnarchiveHabitsCommand(habitList, adapter.getSelectedHabits())) + for (habit in adapter.getSelectedHabits()) { + commandRunner.run(RefreshParentGroupCommand(habit, habitGroupList)) + } adapter.clearSelection() } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/EditHabitCommandTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/EditHabitCommandTest.kt index 72f4a0d06..d17ab7e86 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/EditHabitCommandTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/EditHabitCommandTest.kt @@ -52,7 +52,7 @@ class EditHabitCommandTest : BaseUnitTest() { @Test fun testExecute() { - command = EditHabitCommand(habitList, habit.id!!, modified) + command = EditHabitCommand(habitList, habit.uuid!!, modified) val originalScore = habit.scores[today].value assertThat(habit.name, equalTo("original")) command.run() diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt index 26dd82df8..96648b58b 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt @@ -67,6 +67,7 @@ class ListHabitsBehaviorTest : BaseUnitTest() { clearInvocations(habitList) behavior = ListHabitsBehavior( habitList, + habitGroupList, dirFinder, taskRunner, screen, From 186d672141e45e420088d4ad8a6be1b66e4b8d9a Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sun, 7 Jul 2024 16:59:26 +0200 Subject: [PATCH 31/51] Implement all widgets for sub habits --- uhabits-android/src/main/AndroidManifest.xml | 9 -------- .../habits/list/ListHabitsActivity.kt | 2 +- .../isoron/uhabits/intents/IntentParser.kt | 7 ++++++- .../uhabits/widgets/BaseWidgetProvider.kt | 7 +++++-- .../widgets/activities/HabitPickerDialog.kt | 21 +++++++++++-------- .../src/main/res/xml/widget_streak_info.xml | 2 +- 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/uhabits-android/src/main/AndroidManifest.xml b/uhabits-android/src/main/AndroidManifest.xml index 36f3928c0..bfebccaa2 100644 --- a/uhabits-android/src/main/AndroidManifest.xml +++ b/uhabits-android/src/main/AndroidManifest.xml @@ -110,15 +110,6 @@ - - - - - - (selectedIds.size) for (id in selectedIds) { - val h = habits.getById(id) ?: throw HabitNotFoundException() + val h = habits.getById(id) ?: habitGroups.getHabitByID(id) + ?: throw HabitNotFoundException() selectedHabits.add(h) } return selectedHabits @@ -159,6 +161,7 @@ abstract class BaseWidgetProvider : AppWidgetProvider() { private fun updateDependencies(context: Context) { val app = context.applicationContext as HabitsApplication habits = app.component.habitList + habitGroups = app.component.habitGroupList preferences = app.component.preferences widgetPrefs = app.component.widgetPreferences } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/HabitPickerDialog.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/HabitPickerDialog.kt index 67ad8ae2d..9a9f286ac 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/HabitPickerDialog.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/HabitPickerDialog.kt @@ -25,7 +25,6 @@ import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID import android.content.Intent import android.os.Bundle import android.widget.ArrayAdapter -import android.widget.Button import android.widget.ListView import android.widget.TextView import org.isoron.uhabits.HabitsApplication @@ -34,11 +33,6 @@ import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.core.preferences.WidgetPreferences import org.isoron.uhabits.widgets.WidgetUpdater -class BooleanHabitPickerDialog : HabitPickerDialog() { - override fun shouldHideNumerical() = true - override fun getEmptyMessage() = R.string.no_boolean_habits -} - class NumericalHabitPickerDialog : HabitPickerDialog() { override fun shouldHideBoolean() = true override fun getEmptyMessage() = R.string.no_numerical_habits @@ -50,7 +44,6 @@ open class HabitPickerDialog : Activity() { private lateinit var widgetPreferences: WidgetPreferences private lateinit var widgetUpdater: WidgetUpdater - protected open fun shouldHideNumerical() = false protected open fun shouldHideBoolean() = false protected open fun getEmptyMessage() = R.string.no_habits @@ -59,6 +52,7 @@ open class HabitPickerDialog : Activity() { val component = (applicationContext as HabitsApplication).component AndroidThemeSwitcher(this, component.preferences).apply() val habitList = component.habitList + val habitGroupList = component.habitGroupList widgetPreferences = component.widgetPreferences widgetUpdater = component.widgetUpdater widgetId = intent.extras?.getInt(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID) ?: 0 @@ -67,12 +61,22 @@ open class HabitPickerDialog : Activity() { val habitNames = ArrayList() for (h in habitList) { if (h.isArchived) continue - if (h.isNumerical and shouldHideNumerical()) continue if (!h.isNumerical and shouldHideBoolean()) continue habitIds.add(h.id!!) habitNames.add(h.name) } + for (hgr in habitGroupList) { + if (hgr.isArchived) continue + + for (h in hgr.habitList) { + if (h.isArchived) continue + if (!h.isNumerical and shouldHideBoolean()) continue + habitIds.add(h.id!!) + habitNames.add(h.name) + } + } + if (habitNames.isEmpty()) { setContentView(R.layout.widget_empty_activity) findViewById(R.id.message).setText(getEmptyMessage()) @@ -81,7 +85,6 @@ open class HabitPickerDialog : Activity() { setContentView(R.layout.widget_configure_activity) val listView = findViewById(R.id.listView) - val saveButton = findViewById