mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-14 04:58:52 -06:00
Implement all relevant card views for habit groups
This commit is contained in:
@@ -110,6 +110,15 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.BooleanHabitPickerDialog"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.NumericalHabitPickerDialog"
|
||||
android:exported="true"
|
||||
|
||||
@@ -25,8 +25,29 @@ class ShowHabitGroupView(context: Context) : FrameLayout(context) {
|
||||
binding.subtitleCard.setState(data.subtitle)
|
||||
binding.overviewCard.setState(data.overview)
|
||||
binding.notesCard.setState(data.notes)
|
||||
binding.targetCard.setState(data.target)
|
||||
binding.streakCard.setState(data.streaks)
|
||||
binding.scoreCard.setState(data.scores)
|
||||
binding.barCard.setState(data.bar)
|
||||
binding.frequencyCard.setState(data.frequency)
|
||||
if (!data.isBoolean) {
|
||||
binding.streakCard.visibility = GONE
|
||||
if (!data.isNumerical) {
|
||||
binding.barCard.visibility = GONE
|
||||
}
|
||||
}
|
||||
if (!data.isNumerical) {
|
||||
binding.targetCard.visibility = GONE
|
||||
}
|
||||
if (data.isEmpty) {
|
||||
binding.targetCard.visibility = GONE
|
||||
binding.barCard.visibility = GONE
|
||||
binding.streakCard.visibility = GONE
|
||||
binding.overviewCard.visibility = GONE
|
||||
binding.scoreCard.visibility = GONE
|
||||
binding.frequencyCard.visibility = GONE
|
||||
binding.noSubHabitsCard.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
fun setListener(presenter: ShowHabitGroupPresenter) {
|
||||
|
||||
@@ -33,6 +33,11 @@ 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
|
||||
@@ -44,6 +49,7 @@ 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
|
||||
|
||||
@@ -61,6 +67,7 @@ open class HabitPickerDialog : Activity() {
|
||||
val habitNames = ArrayList<String>()
|
||||
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)
|
||||
|
||||
@@ -57,15 +57,44 @@
|
||||
style="@style/Card"
|
||||
android:paddingTop="12dp"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/noSubHabitsCard"
|
||||
style="@style/Card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_sub_habits" />
|
||||
</FrameLayout>
|
||||
|
||||
<org.isoron.uhabits.activities.habits.show.views.TargetCardView
|
||||
android:id="@+id/targetCard"
|
||||
style="@style/Card"
|
||||
android:paddingTop="12dp"/>
|
||||
|
||||
<org.isoron.uhabits.activities.habits.show.views.ScoreCardView
|
||||
android:id="@+id/scoreCard"
|
||||
style="@style/Card"
|
||||
android:gravity="center"/>
|
||||
|
||||
<org.isoron.uhabits.activities.habits.show.views.BarCardView
|
||||
android:id="@+id/barCard"
|
||||
style="@style/Card"
|
||||
android:gravity="center"/>
|
||||
|
||||
<org.isoron.uhabits.activities.habits.show.views.StreakCardView
|
||||
android:id="@+id/streakCard"
|
||||
style="@style/Card"/>
|
||||
|
||||
<org.isoron.uhabits.activities.habits.show.views.FrequencyCardView
|
||||
android:id="@+id/frequencyCard"
|
||||
style="@style/Card"/>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -237,4 +237,5 @@
|
||||
<string name="activity_not_found">No app was found to support this action</string>
|
||||
<string name="pref_midnight_delay_title">Extend day a few hours past midnight</string>
|
||||
<string name="pref_midnight_delay_description">Wait until 3:00 AM to show a new day. Useful if you typically go to sleep after midnight. Requires app restart.</string>
|
||||
<string name="no_sub_habits">Group contains no habits.</string>
|
||||
</resources>
|
||||
|
||||
@@ -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">
|
||||
|
||||
</appwidget-provider>
|
||||
@@ -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<Entry>.groupedSum(
|
||||
}
|
||||
}
|
||||
|
||||
fun List<Entry>.groupedSum(): List<Entry> {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 <K, V> mergeMaps(map1: HashMap<K, V>, map2: HashMap<K, V>, mergeFunction: (V, V) -> V): HashMap<K, V> {
|
||||
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<Int>, array2: Array<Int>): Array<Int> {
|
||||
return array1.zip(array2) { a, b -> a + b }.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Timestamp, Timestamp> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user