Implement all relevant card views for habit groups

pull/2020/head
Dharanish 1 year ago
parent 186d672141
commit 30b124ae6e

@ -110,6 +110,15 @@
</intent-filter> </intent-filter>
</activity> </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 <activity
android:name=".widgets.activities.NumericalHabitPickerDialog" android:name=".widgets.activities.NumericalHabitPickerDialog"
android:exported="true" android:exported="true"

@ -25,8 +25,29 @@ class ShowHabitGroupView(context: Context) : FrameLayout(context) {
binding.subtitleCard.setState(data.subtitle) binding.subtitleCard.setState(data.subtitle)
binding.overviewCard.setState(data.overview) binding.overviewCard.setState(data.overview)
binding.notesCard.setState(data.notes) binding.notesCard.setState(data.notes)
binding.targetCard.setState(data.target)
binding.streakCard.setState(data.streaks) binding.streakCard.setState(data.streaks)
binding.scoreCard.setState(data.scores) 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) { 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.core.preferences.WidgetPreferences
import org.isoron.uhabits.widgets.WidgetUpdater import org.isoron.uhabits.widgets.WidgetUpdater
class BooleanHabitPickerDialog : HabitPickerDialog() {
override fun shouldHideNumerical() = true
override fun getEmptyMessage() = R.string.no_boolean_habits
}
class NumericalHabitPickerDialog : HabitPickerDialog() { class NumericalHabitPickerDialog : HabitPickerDialog() {
override fun shouldHideBoolean() = true override fun shouldHideBoolean() = true
override fun getEmptyMessage() = R.string.no_numerical_habits override fun getEmptyMessage() = R.string.no_numerical_habits
@ -44,6 +49,7 @@ open class HabitPickerDialog : Activity() {
private lateinit var widgetPreferences: WidgetPreferences private lateinit var widgetPreferences: WidgetPreferences
private lateinit var widgetUpdater: WidgetUpdater private lateinit var widgetUpdater: WidgetUpdater
protected open fun shouldHideNumerical() = false
protected open fun shouldHideBoolean() = false protected open fun shouldHideBoolean() = false
protected open fun getEmptyMessage() = R.string.no_habits protected open fun getEmptyMessage() = R.string.no_habits
@ -61,6 +67,7 @@ open class HabitPickerDialog : Activity() {
val habitNames = ArrayList<String>() val habitNames = ArrayList<String>()
for (h in habitList) { for (h in habitList) {
if (h.isArchived) continue if (h.isArchived) continue
if (h.isNumerical and shouldHideNumerical()) continue
if (!h.isNumerical and shouldHideBoolean()) continue if (!h.isNumerical and shouldHideBoolean()) continue
habitIds.add(h.id!!) habitIds.add(h.id!!)
habitNames.add(h.name) habitNames.add(h.name)

@ -57,15 +57,44 @@
style="@style/Card" style="@style/Card"
android:paddingTop="12dp"/> 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 <org.isoron.uhabits.activities.habits.show.views.ScoreCardView
android:id="@+id/scoreCard" android:id="@+id/scoreCard"
style="@style/Card" style="@style/Card"
android:gravity="center"/> 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 <org.isoron.uhabits.activities.habits.show.views.StreakCardView
android:id="@+id/streakCard" android:id="@+id/streakCard"
style="@style/Card"/> style="@style/Card"/>
<org.isoron.uhabits.activities.habits.show.views.FrequencyCardView
android:id="@+id/frequencyCard"
style="@style/Card"/>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

@ -237,4 +237,5 @@
<string name="activity_not_found">No app was found to support this action</string> <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_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="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> </resources>

@ -27,7 +27,7 @@
android:previewImage="@drawable/widget_preview_streaks" android:previewImage="@drawable/widget_preview_streaks"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"
android:updatePeriodMillis="3600000" android:updatePeriodMillis="3600000"
android:configure="org.isoron.uhabits.widgets.activities.HabitPickerDialog" android:configure="org.isoron.uhabits.widgets.activities.BooleanHabitPickerDialog"
android:widgetCategory="home_screen"> android:widgetCategory="home_screen">
</appwidget-provider> </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_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
import java.util.ArrayList
import java.util.Calendar import java.util.Calendar
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.set import kotlin.collections.set
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt
@ThreadSafe @ThreadSafe
open class EntryList { open class EntryList {
@ -151,6 +151,22 @@ open class EntryList {
return map 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) { data class Interval(val begin: Timestamp, val center: Timestamp, val end: Timestamp) {
val length: Int val length: Int
get() = begin.daysUntil(end) + 1 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. * 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.HabitGroup
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences 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.NotesCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardState 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.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.StreakCartPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardPresenter 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.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 import org.isoron.uhabits.core.ui.views.Theme
data class ShowHabitGroupState( data class ShowHabitGroupState(
val title: String = "", val title: String = "",
val isEmpty: Boolean = false,
val isNumerical: Boolean = false,
val isBoolean: Boolean = false,
val color: PaletteColor = PaletteColor(1), val color: PaletteColor = PaletteColor(1),
val subtitle: SubtitleCardState, val subtitle: SubtitleCardState,
val overview: OverviewCardState, val overview: OverviewCardState,
val notes: NotesCardState, val notes: NotesCardState,
val target: TargetCardState,
val streaks: StreakCardState, val streaks: StreakCardState,
val scores: ScoreCardState, val scores: ScoreCardState,
val frequency: FrequencyCardState,
val bar: BarCardState,
val theme: Theme val theme: Theme
) )
@ -33,6 +45,12 @@ class ShowHabitGroupPresenter(
val screen: Screen, val screen: Screen,
val commandRunner: CommandRunner val commandRunner: CommandRunner
) { ) {
val barCardPresenter = BarCardPresenter(
preferences = preferences,
screen = screen
)
val scoreCardPresenter = ScoreCardPresenter( val scoreCardPresenter = ScoreCardPresenter(
preferences = preferences, preferences = preferences,
screen = screen screen = screen
@ -46,6 +64,9 @@ class ShowHabitGroupPresenter(
): ShowHabitGroupState { ): ShowHabitGroupState {
return ShowHabitGroupState( return ShowHabitGroupState(
title = habitGroup.name, title = habitGroup.name,
isEmpty = habitGroup.habitList.isEmpty,
isNumerical = habitGroup.habitList.all { it.isNumerical },
isBoolean = habitGroup.habitList.all { !it.isNumerical },
color = habitGroup.color, color = habitGroup.color,
theme = theme, theme = theme,
subtitle = SubtitleCardPresenter.buildState( subtitle = SubtitleCardPresenter.buildState(
@ -59,6 +80,11 @@ class ShowHabitGroupPresenter(
notes = NotesCardPresenter.buildState( notes = NotesCardPresenter.buildState(
habitGroup = habitGroup habitGroup = habitGroup
), ),
target = TargetCardPresenter.buildState(
habitGroup = habitGroup,
firstWeekday = preferences.firstWeekdayInt,
theme = theme
),
streaks = StreakCartPresenter.buildState( streaks = StreakCartPresenter.buildState(
habitGroup = habitGroup, habitGroup = habitGroup,
theme = theme theme = theme
@ -68,11 +94,24 @@ class ShowHabitGroupPresenter(
habitGroup = habitGroup, habitGroup = habitGroup,
firstWeekday = preferences.firstWeekdayInt, firstWeekday = preferences.firstWeekdayInt,
theme = theme 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 : interface Screen :
BarCardPresenter.Screen,
ScoreCardPresenter.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.Entry
import org.isoron.uhabits.core.models.Habit 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.PaletteColor
import org.isoron.uhabits.core.models.groupedSum import org.isoron.uhabits.core.models.groupedSum
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
@ -74,6 +75,53 @@ class BarCardPresenter(
boolSpinnerPosition = boolSpinnerPosition 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) { fun onNumericalSpinnerPosition(position: Int) {

@ -20,10 +20,10 @@
package org.isoron.uhabits.core.ui.screens.habits.show.views package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit 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.PaletteColor
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.ui.views.Theme import org.isoron.uhabits.core.ui.views.Theme
import java.util.HashMap
data class FrequencyCardState( data class FrequencyCardState(
val color: PaletteColor, val color: PaletteColor,
@ -48,5 +48,42 @@ class FrequencyCardPresenter {
firstWeekday = firstWeekday, firstWeekday = firstWeekday,
theme = theme 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 package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit 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.PaletteColor
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.models.countSkippedDays import org.isoron.uhabits.core.models.countSkippedDays
@ -176,5 +177,66 @@ class TargetCardPresenter {
if (thisWeek.daysUntil(newest) < 7) newest = thisWeek.plus(7) if (thisWeek.daysUntil(newest) < 7) newest = thisWeek.plus(7)
return Pair(yearBegin, newest) 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)
} }
} }

Loading…
Cancel
Save