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)
}
}