From 30b124ae6ef4f2ea727812013fb34e8391695c15 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Tue, 9 Jul 2024 12:14:04 +0200 Subject: [PATCH] Implement all relevant card views for habit groups --- uhabits-android/src/main/AndroidManifest.xml | 9 +++ .../habits/show/ShowHabitGroupView.kt | 21 +++++++ .../widgets/activities/HabitPickerDialog.kt | 7 +++ .../src/main/res/layout/show_habit_group.xml | 29 +++++++++ .../src/main/res/values/strings.xml | 1 + .../src/main/res/xml/widget_streak_info.xml | 2 +- .../isoron/uhabits/core/models/EntryList.kt | 28 ++++++++- .../ui/screens/habits/show/ShowHabitGroup.kt | 39 ++++++++++++ .../ui/screens/habits/show/views/BarCard.kt | 48 ++++++++++++++ .../habits/show/views/FrequencyCard.kt | 39 +++++++++++- .../screens/habits/show/views/TargetCard.kt | 62 +++++++++++++++++++ 11 files changed, 282 insertions(+), 3 deletions(-) diff --git a/uhabits-android/src/main/AndroidManifest.xml b/uhabits-android/src/main/AndroidManifest.xml index bfebccaa2..36f3928c0 100644 --- a/uhabits-android/src/main/AndroidManifest.xml +++ b/uhabits-android/src/main/AndroidManifest.xml @@ -110,6 +110,15 @@ + + + + + + () 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) diff --git a/uhabits-android/src/main/res/layout/show_habit_group.xml b/uhabits-android/src/main/res/layout/show_habit_group.xml index b8b433577..cabeb947c 100644 --- a/uhabits-android/src/main/res/layout/show_habit_group.xml +++ b/uhabits-android/src/main/res/layout/show_habit_group.xml @@ -57,15 +57,44 @@ style="@style/Card" android:paddingTop="12dp"/> + + + + + + + + + + + diff --git a/uhabits-android/src/main/res/values/strings.xml b/uhabits-android/src/main/res/values/strings.xml index f966fdb5d..94da4536e 100644 --- a/uhabits-android/src/main/res/values/strings.xml +++ b/uhabits-android/src/main/res/values/strings.xml @@ -237,4 +237,5 @@ No app was found to support this action Extend day a few hours past midnight Wait until 3:00 AM to show a new day. Useful if you typically go to sleep after midnight. Requires app restart. + Group contains no habits. diff --git a/uhabits-android/src/main/res/xml/widget_streak_info.xml b/uhabits-android/src/main/res/xml/widget_streak_info.xml index 9f0d33298..236d2b784 100644 --- a/uhabits-android/src/main/res/xml/widget_streak_info.xml +++ b/uhabits-android/src/main/res/xml/widget_streak_info.xml @@ -27,7 +27,7 @@ android:previewImage="@drawable/widget_preview_streaks" android:resizeMode="vertical|horizontal" android:updatePeriodMillis="3600000" - android:configure="org.isoron.uhabits.widgets.activities.HabitPickerDialog" + android:configure="org.isoron.uhabits.widgets.activities.BooleanHabitPickerDialog" android:widgetCategory="home_screen"> \ 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 5c5499f49..820a1618c 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 @@ -24,12 +24,12 @@ import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.utils.DateUtils -import java.util.ArrayList import java.util.Calendar import javax.annotation.concurrent.ThreadSafe import kotlin.collections.set import kotlin.math.max import kotlin.math.min +import kotlin.math.roundToInt @ThreadSafe open class EntryList { @@ -151,6 +151,22 @@ open class EntryList { return map } + @Synchronized fun normalizeEntries( + isNumerical: Boolean, + frequency: Frequency, + targetValue: Double + ): EntryList { + val entries = getKnown() + val normalized = EntryList() + val dailyTarget = frequency.toDouble() * (if (isNumerical) targetValue else 0.001) + for (entry in entries) { + if (!isNumerical && entry.value != YES_MANUAL) continue + val newValue = (entry.value.toDouble() / dailyTarget).roundToInt() + normalized.add(Entry(entry.timestamp, newValue)) + } + return normalized + } + data class Interval(val begin: Timestamp, val center: Timestamp, val end: Timestamp) { val length: Int get() = begin.daysUntil(end) + 1 @@ -318,6 +334,16 @@ fun List.groupedSum( } } +fun List.groupedSum(): List { + return this + .groupBy { entry -> entry.timestamp } + .entries.map { (timestamp, entries) -> + Entry(timestamp, entries.sumOf { it.value }) + }.sortedBy { (timestamp, _) -> + -timestamp.unixTime + } +} + /** * Counts the number of days with vaLue SKIP in the given period. */ 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 index 56359832e..647f735dd 100644 --- 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 @@ -4,6 +4,10 @@ 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.BarCardPresenter +import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardState +import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardPresenter +import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardState 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 @@ -14,16 +18,24 @@ 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.screens.habits.show.views.TargetCardPresenter +import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardState import org.isoron.uhabits.core.ui.views.Theme data class ShowHabitGroupState( val title: String = "", + val isEmpty: Boolean = false, + val isNumerical: Boolean = false, + val isBoolean: Boolean = false, val color: PaletteColor = PaletteColor(1), val subtitle: SubtitleCardState, val overview: OverviewCardState, val notes: NotesCardState, + val target: TargetCardState, val streaks: StreakCardState, val scores: ScoreCardState, + val frequency: FrequencyCardState, + val bar: BarCardState, val theme: Theme ) @@ -33,6 +45,12 @@ class ShowHabitGroupPresenter( val screen: Screen, val commandRunner: CommandRunner ) { + + val barCardPresenter = BarCardPresenter( + preferences = preferences, + screen = screen + ) + val scoreCardPresenter = ScoreCardPresenter( preferences = preferences, screen = screen @@ -46,6 +64,9 @@ class ShowHabitGroupPresenter( ): ShowHabitGroupState { return ShowHabitGroupState( title = habitGroup.name, + isEmpty = habitGroup.habitList.isEmpty, + isNumerical = habitGroup.habitList.all { it.isNumerical }, + isBoolean = habitGroup.habitList.all { !it.isNumerical }, color = habitGroup.color, theme = theme, subtitle = SubtitleCardPresenter.buildState( @@ -59,6 +80,11 @@ class ShowHabitGroupPresenter( notes = NotesCardPresenter.buildState( habitGroup = habitGroup ), + target = TargetCardPresenter.buildState( + habitGroup = habitGroup, + firstWeekday = preferences.firstWeekdayInt, + theme = theme + ), streaks = StreakCartPresenter.buildState( habitGroup = habitGroup, theme = theme @@ -68,11 +94,24 @@ class ShowHabitGroupPresenter( habitGroup = habitGroup, firstWeekday = preferences.firstWeekdayInt, theme = theme + ), + frequency = FrequencyCardPresenter.buildState( + habitGroup = habitGroup, + firstWeekday = preferences.firstWeekdayInt, + theme = theme + ), + bar = BarCardPresenter.buildState( + habitGroup = habitGroup, + firstWeekday = preferences.firstWeekdayInt, + boolSpinnerPosition = preferences.barCardBoolSpinnerPosition, + numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition, + theme = theme ) ) } } interface Screen : + BarCardPresenter.Screen, ScoreCardPresenter.Screen } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt index 303b33cee..29289781d 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.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.models.groupedSum import org.isoron.uhabits.core.preferences.Preferences @@ -74,6 +75,53 @@ class BarCardPresenter( boolSpinnerPosition = boolSpinnerPosition ) } + + fun buildState( + habitGroup: HabitGroup, + firstWeekday: Int, + numericalSpinnerPosition: Int, + boolSpinnerPosition: Int, + theme: Theme + ): BarCardState { + val isNumerical = habitGroup.habitList.all { it.isNumerical } + val isBoolean = habitGroup.habitList.all { !it.isNumerical } + if ((!isNumerical && !isBoolean) || habitGroup.habitList.isEmpty) { + return BarCardState( + theme = theme, + entries = listOf(Entry(DateUtils.getTodayWithOffset(), 0)), + bucketSize = 1, + color = habitGroup.color, + isNumerical = isNumerical, + numericalSpinnerPosition = numericalSpinnerPosition, + boolSpinnerPosition = boolSpinnerPosition + ) + } + val bucketSize = if (isNumerical) { + numericalBucketSizes[numericalSpinnerPosition] + } else { + boolBucketSizes[boolSpinnerPosition] + } + val today = DateUtils.getTodayWithOffset() + val allEntries = habitGroup.habitList.map { habit -> + val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today + habit.computedEntries.getByInterval(oldest, today).groupedSum( + truncateField = ScoreCardPresenter.getTruncateField(bucketSize), + firstWeekday = firstWeekday, + isNumerical = habit.isNumerical + ) + }.flatten() + + val summedEntries = allEntries.groupedSum() + return BarCardState( + theme = theme, + entries = summedEntries, + bucketSize = bucketSize, + color = habitGroup.color, + isNumerical = isNumerical, + numericalSpinnerPosition = numericalSpinnerPosition, + boolSpinnerPosition = boolSpinnerPosition + ) + } } fun onNumericalSpinnerPosition(position: Int) { 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 8a233109e..66a71b1bd 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 @@ -20,10 +20,10 @@ 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.Timestamp import org.isoron.uhabits.core.ui.views.Theme -import java.util.HashMap data class FrequencyCardState( val color: PaletteColor, @@ -48,5 +48,42 @@ class FrequencyCardPresenter { firstWeekday = firstWeekday, theme = theme ) + + fun buildState( + habitGroup: HabitGroup, + firstWeekday: Int, + theme: Theme + ): FrequencyCardState { + val normalizedEntries = habitGroup.habitList.map { + it.computedEntries.normalizeEntries(it.isNumerical, it.frequency, it.targetValue) + } + val frequencies = normalizedEntries.map { + it.computeWeekdayFrequency(isNumerical = true) + }.reduce { acc, hashMap -> + mergeMaps(acc, hashMap) { value1, value2 -> addArray(value1, value2) } + } + + return FrequencyCardState( + color = habitGroup.color, + isNumerical = true, + frequency = frequencies, + firstWeekday = firstWeekday, + theme = theme + ) + } + + private fun mergeMaps(map1: HashMap, map2: HashMap, mergeFunction: (V, V) -> V): HashMap { + val result = map1 // Step 1 + for ((key, value) in map2) { // Step 2 + result[key] = result[key]?.let { existingValue -> + mergeFunction(existingValue, value) // Step 3 (merge logic) + } ?: value + } + return result // Step 4 + } + + private fun addArray(array1: Array, array2: Array): Array { + return array1.zip(array2) { a, b -> a + b }.toTypedArray() + } } } 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 f137de23e..361dfb2e9 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 @@ -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.Timestamp import org.isoron.uhabits.core.models.countSkippedDays @@ -176,5 +177,66 @@ class TargetCardPresenter { if (thisWeek.daysUntil(newest) < 7) newest = thisWeek.plus(7) return Pair(yearBegin, newest) } + + fun buildState( + habitGroup: HabitGroup, + firstWeekday: Int, + theme: Theme + ): TargetCardState { + val maxDen = habitGroup.habitList.maxOfOrNull { habit -> habit.frequency.denominator } + val isNumerical = habitGroup.habitList.all { it.isNumerical } + if (maxDen == null || !isNumerical) { + return TargetCardState( + color = habitGroup.color, + values = arrayListOf(0.0, 0.0, 0.0, 0.0, 0.0), + targets = arrayListOf(0.0, 0.0, 0.0, 0.0, 0.0), + intervals = arrayListOf(1, 7, 30, 91, 365), + theme = theme + ) + } + + val states = habitGroup.habitList.map { Companion.buildState(it, firstWeekday, theme) } + + val values = states + .map { + val startIdx = it.intervals.indexOf(maxDen) + val endIdx = it.intervals.size + it.values.subList(startIdx, endIdx) + } + .reduce { acc, list -> + acc.zip(list) { a, b -> a + b } + } + + val targets = states + .map { + val startIdx = it.intervals.indexOf(maxDen) + val endIdx = it.intervals.size + it.targets.subList(startIdx, endIdx) + } + .reduce { acc, list -> + acc.zip(list) { a, b -> a + b } + } + + val intervals = arrayListOf(1, 7, 30, 91, 365).filter { it >= maxDen } + + return TargetCardState( + color = habitGroup.color, + values = values, + targets = targets, + intervals = intervals, + 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) } }