mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Merge branch 'dev' into feature/sync
This commit is contained in:
@@ -43,13 +43,13 @@ kotlin {
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
compileOnly("com.google.dagger:dagger:2.35.1")
|
||||
compileOnly("com.google.dagger:dagger:2.38.1")
|
||||
implementation("com.google.guava:guava:30.1.1-android")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.2")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.21")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1")
|
||||
implementation("androidx.annotation:annotation:1.2.0")
|
||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||
implementation("com.opencsv:opencsv:5.4")
|
||||
implementation("com.opencsv:opencsv:5.5.1")
|
||||
implementation("commons-codec:commons-codec:1.15")
|
||||
implementation("org.apache.commons:commons-lang3:3.12.0")
|
||||
}
|
||||
@@ -59,7 +59,7 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation(kotlin("test-junit"))
|
||||
implementation("org.xerial:sqlite-jdbc:3.34.0")
|
||||
implementation("org.xerial:sqlite-jdbc:3.36.0.1")
|
||||
implementation("org.hamcrest:hamcrest:2.2")
|
||||
implementation("org.apache.commons:commons-io:1.3.2")
|
||||
implementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||
@@ -67,3 +67,10 @@ kotlin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<org.gradle.language.jvm.tasks.ProcessResources>("jvmProcessResources") {
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
}
|
||||
tasks.named<org.gradle.language.jvm.tasks.ProcessResources>("jvmTestProcessResources") {
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
}
|
||||
|
||||
@@ -72,10 +72,20 @@ data class LocalDate(val daysSince2000: Int) {
|
||||
return dayCache
|
||||
}
|
||||
|
||||
val monthLength: Int
|
||||
get() = when (month) {
|
||||
4, 6, 9, 11 -> 30
|
||||
2 -> if (isLeapYear(year)) 29 else 28
|
||||
else -> 31
|
||||
}
|
||||
|
||||
private fun updateYearMonthDayCache() {
|
||||
var currYear = 2000
|
||||
var currDay = 0
|
||||
|
||||
if (daysSince2000 < 0) {
|
||||
currYear -= 400
|
||||
currDay -= 146097
|
||||
}
|
||||
while (true) {
|
||||
val currYearLength = if (isLeapYear(currYear)) 366 else 365
|
||||
if (daysSince2000 < currDay + currYearLength) {
|
||||
@@ -86,10 +96,8 @@ data class LocalDate(val daysSince2000: Int) {
|
||||
currDay += currYearLength
|
||||
}
|
||||
}
|
||||
|
||||
var currMonth = 1
|
||||
val monthOffset = if (isLeapYear(currYear)) leapOffset else nonLeapOffset
|
||||
|
||||
while (true) {
|
||||
if (daysSince2000 < currDay + monthOffset[currMonth]) {
|
||||
monthCache = currMonth
|
||||
@@ -98,7 +106,6 @@ data class LocalDate(val daysSince2000: Int) {
|
||||
currMonth++
|
||||
}
|
||||
}
|
||||
|
||||
currDay += monthOffset[currMonth - 1]
|
||||
dayCache = daysSince2000 - currDay + 1
|
||||
}
|
||||
|
||||
@@ -248,8 +248,17 @@ open class EntryList {
|
||||
for (i in num - 1 until filtered.size) {
|
||||
val (begin, _) = filtered[i]
|
||||
val (center, _) = filtered[i - num + 1]
|
||||
if (begin.daysUntil(center) < den) {
|
||||
val end = begin.plus(den - 1)
|
||||
var size = den
|
||||
if (den == 30 || den == 31) {
|
||||
val beginDate = begin.toLocalDate()
|
||||
size = if (beginDate.day == beginDate.monthLength) {
|
||||
beginDate.plus(1).monthLength
|
||||
} else {
|
||||
beginDate.monthLength
|
||||
}
|
||||
}
|
||||
if (begin.daysUntil(center) < size) {
|
||||
val end = begin.plus(size - 1)
|
||||
intervals.add(Interval(begin, center, end))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,9 @@ data class Habit(
|
||||
var position: Int = 0,
|
||||
var question: String = "",
|
||||
var reminder: Reminder? = null,
|
||||
var targetType: Int = AT_LEAST,
|
||||
var targetType: NumericalHabitType = NumericalHabitType.AT_LEAST,
|
||||
var targetValue: Double = 0.0,
|
||||
var type: Int = YES_NO_HABIT,
|
||||
var type: HabitType = HabitType.YES_NO,
|
||||
var unit: String = "",
|
||||
var uuid: String? = null,
|
||||
val computedEntries: EntryList,
|
||||
@@ -48,7 +48,7 @@ data class Habit(
|
||||
var observable = ModelObservable()
|
||||
|
||||
val isNumerical: Boolean
|
||||
get() = type == NUMBER_HABIT
|
||||
get() = type == HabitType.NUMERICAL
|
||||
|
||||
val uriString: String
|
||||
get() = "content://org.isoron.uhabits/habit/$id"
|
||||
@@ -59,16 +59,28 @@ data class Habit(
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
val value = computedEntries.get(today).value
|
||||
return if (isNumerical) {
|
||||
if (targetType == AT_LEAST) {
|
||||
value / 1000.0 >= targetValue
|
||||
} else {
|
||||
value / 1000.0 <= targetValue
|
||||
when (targetType) {
|
||||
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
|
||||
NumericalHabitType.AT_MOST -> value / 1000.0 <= targetValue
|
||||
}
|
||||
} else {
|
||||
value != Entry.NO && value != Entry.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
fun isFailedToday(): Boolean {
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
val value = computedEntries.get(today).value
|
||||
return if (isNumerical) {
|
||||
when (targetType) {
|
||||
NumericalHabitType.AT_LEAST -> value / 1000.0 < targetValue
|
||||
NumericalHabitType.AT_MOST -> value / 1000.0 > targetValue
|
||||
}
|
||||
} else {
|
||||
value == Entry.NO
|
||||
}
|
||||
}
|
||||
|
||||
fun recompute() {
|
||||
computedEntries.recomputeFrom(
|
||||
originalEntries = originalEntries,
|
||||
@@ -146,18 +158,11 @@ data class Habit(
|
||||
result = 31 * result + position
|
||||
result = 31 * result + question.hashCode()
|
||||
result = 31 * result + (reminder?.hashCode() ?: 0)
|
||||
result = 31 * result + targetType
|
||||
result = 31 * result + targetType.value
|
||||
result = 31 * result + targetValue.hashCode()
|
||||
result = 31 * result + type
|
||||
result = 31 * result + type.value
|
||||
result = 31 * result + unit.hashCode()
|
||||
result = 31 * result + (uuid?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val AT_LEAST = 0
|
||||
const val AT_MOST = 1
|
||||
const val NUMBER_HABIT = 1
|
||||
const val YES_NO_HABIT = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ data class HabitMatcher(
|
||||
fun matches(habit: Habit): Boolean {
|
||||
if (!isArchivedAllowed && habit.isArchived) return false
|
||||
if (isReminderRequired && !habit.hasReminder()) return false
|
||||
if (!isCompletedAllowed && habit.isCompletedToday()) return false
|
||||
if (!isCompletedAllowed && (habit.isCompletedToday() || habit.isFailedToday())) return false
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.isoron.uhabits.core.models
|
||||
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
enum class HabitType(val value: Int) {
|
||||
YES_NO(0), NUMERICAL(1);
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int): HabitType {
|
||||
return when (value) {
|
||||
YES_NO.value -> YES_NO
|
||||
NUMERICAL.value -> NUMERICAL
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.isoron.uhabits.core.models
|
||||
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
enum class NumericalHabitType(val value: Int) {
|
||||
AT_LEAST(0), AT_MOST(1);
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int): NumericalHabitType {
|
||||
return when (value) {
|
||||
AT_LEAST.value -> AT_LEAST
|
||||
AT_MOST.value -> AT_MOST
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ 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
|
||||
|
||||
@ThreadSafe
|
||||
@@ -93,11 +94,15 @@ class ScoreList {
|
||||
for (i in values.indices) {
|
||||
val offset = values.size - i - 1
|
||||
if (isNumerical) {
|
||||
rollingSum += values[offset]
|
||||
rollingSum += max(0, values[offset])
|
||||
if (offset + denominator < values.size) {
|
||||
rollingSum -= values[offset + denominator]
|
||||
}
|
||||
val percentageCompleted = min(1.0, rollingSum / 1000 / targetValue)
|
||||
val percentageCompleted = if (targetValue > 0) {
|
||||
min(1.0, rollingSum / 1000 / targetValue)
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
previousValue = compute(freq, previousValue, percentageCompleted)
|
||||
} else {
|
||||
if (values[offset] == Entry.YES_MANUAL) {
|
||||
|
||||
@@ -22,10 +22,12 @@ import org.isoron.uhabits.core.database.Column
|
||||
import org.isoron.uhabits.core.database.Table
|
||||
import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
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.WeekdayList
|
||||
import java.util.Objects
|
||||
import java.util.Objects.requireNonNull
|
||||
|
||||
/**
|
||||
* The SQLite database record corresponding to a [Habit].
|
||||
@@ -93,8 +95,8 @@ class HabitRecord {
|
||||
highlight = 0
|
||||
color = model.color.paletteIndex
|
||||
archived = if (model.isArchived) 1 else 0
|
||||
type = model.type
|
||||
targetType = model.targetType
|
||||
type = model.type.value
|
||||
targetType = model.targetType.value
|
||||
targetValue = model.targetValue
|
||||
unit = model.unit
|
||||
position = model.position
|
||||
@@ -108,7 +110,7 @@ class HabitRecord {
|
||||
reminderHour = null
|
||||
if (model.hasReminder()) {
|
||||
val reminder = model.reminder
|
||||
reminderHour = Objects.requireNonNull(reminder)!!.hour
|
||||
reminderHour = requireNonNull(reminder)!!.hour
|
||||
reminderMin = reminder!!.minute
|
||||
reminderDays = reminder.days.toInteger()
|
||||
}
|
||||
@@ -122,8 +124,8 @@ class HabitRecord {
|
||||
habit.frequency = Frequency(freqNum!!, freqDen!!)
|
||||
habit.color = PaletteColor(color!!)
|
||||
habit.isArchived = archived != 0
|
||||
habit.type = type!!
|
||||
habit.targetType = targetType!!
|
||||
habit.type = HabitType.fromInt(type!!)
|
||||
habit.targetType = NumericalHabitType.fromInt(targetType!!)
|
||||
habit.targetValue = targetValue!!
|
||||
habit.unit = unit!!
|
||||
habit.position = position!!
|
||||
|
||||
@@ -226,9 +226,12 @@ open class Preferences(private val storage: Storage) {
|
||||
storage.putString("pref_encryption_key", "")
|
||||
}
|
||||
|
||||
fun areQuestionMarksEnabled(): Boolean {
|
||||
return storage.getBoolean("pref_unknown_enabled", false)
|
||||
}
|
||||
var areQuestionMarksEnabled: Boolean
|
||||
get() = storage.getBoolean("pref_unknown_enabled", false)
|
||||
set(value) {
|
||||
storage.putBoolean("pref_unknown_enabled", value)
|
||||
for (l in listeners) l.onQuestionMarksChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An integer representing the first day of the week. Sunday
|
||||
@@ -261,6 +264,7 @@ open class Preferences(private val storage: Storage) {
|
||||
interface Listener {
|
||||
fun onCheckmarkSequenceChanged() {}
|
||||
fun onNotificationsChanged() {}
|
||||
fun onQuestionMarksChanged() {}
|
||||
fun onSyncEnabled() {}
|
||||
}
|
||||
|
||||
@@ -280,7 +284,7 @@ open class Preferences(private val storage: Storage) {
|
||||
putString(key, joinLongs(values))
|
||||
}
|
||||
|
||||
fun getLongArray(key: String, defValue: LongArray): LongArray? {
|
||||
fun getLongArray(key: String, defValue: LongArray): LongArray {
|
||||
val string = getString(key, "")
|
||||
return if (string.isEmpty()) defValue else splitLongs(
|
||||
string
|
||||
|
||||
@@ -27,19 +27,18 @@ class WidgetPreferences @Inject constructor(private val storage: Preferences.Sto
|
||||
storage.putLongArray(getHabitIdKey(widgetId), habitIds)
|
||||
}
|
||||
|
||||
fun getHabitIdsFromWidgetId(widgetId: Int): LongArray? {
|
||||
var habitIds: LongArray?
|
||||
fun getHabitIdsFromWidgetId(widgetId: Int): LongArray {
|
||||
val habitIdKey = getHabitIdKey(widgetId)
|
||||
try {
|
||||
habitIds = storage.getLongArray(habitIdKey, longArrayOf(-1))
|
||||
return try {
|
||||
storage.getLongArray(habitIdKey, longArrayOf())
|
||||
} catch (e: ClassCastException) {
|
||||
// Up to Loop 1.7.11, this preference was not an array, but a single
|
||||
// long. Trying to read the old preference causes a cast exception.
|
||||
habitIds = LongArray(1)
|
||||
habitIds[0] = storage.getLong(habitIdKey, -1)
|
||||
storage.putLongArray(habitIdKey, habitIds)
|
||||
when (val habitId = storage.getLong(habitIdKey, -1)) {
|
||||
-1L -> longArrayOf()
|
||||
else -> longArrayOf(habitId)
|
||||
}
|
||||
}
|
||||
return habitIds
|
||||
}
|
||||
|
||||
fun removeWidget(id: Int) {
|
||||
|
||||
@@ -31,7 +31,7 @@ class SingleThreadTaskRunner : TaskRunner {
|
||||
|
||||
override fun execute(task: Task) {
|
||||
for (l in listeners) l.onTaskStarted(task)
|
||||
if (!task.isCanceled) {
|
||||
if (!task.isCanceled()) {
|
||||
task.onAttached(this)
|
||||
task.onPreExecute()
|
||||
task.doInBackground()
|
||||
|
||||
@@ -20,9 +20,7 @@ package org.isoron.uhabits.core.tasks
|
||||
|
||||
fun interface Task {
|
||||
fun cancel() {}
|
||||
val isCanceled: Boolean
|
||||
get() = false
|
||||
|
||||
fun isCanceled() = false
|
||||
fun doInBackground()
|
||||
fun onAttached(runner: TaskRunner) {}
|
||||
fun onPostExecute() {}
|
||||
|
||||
@@ -22,7 +22,9 @@ import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitList
|
||||
import org.isoron.uhabits.core.models.HabitType
|
||||
import org.isoron.uhabits.core.models.ModelFactory
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.models.sqlite.SQLiteEntryList
|
||||
@@ -65,11 +67,11 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis
|
||||
|
||||
fun createNumericalHabit(): Habit {
|
||||
val habit = modelFactory.buildHabit()
|
||||
habit.type = Habit.NUMBER_HABIT
|
||||
habit.type = HabitType.NUMERICAL
|
||||
habit.name = "Run"
|
||||
habit.question = "How many miles did you run today?"
|
||||
habit.unit = "miles"
|
||||
habit.targetType = Habit.AT_LEAST
|
||||
habit.targetType = NumericalHabitType.AT_LEAST
|
||||
habit.targetValue = 2.0
|
||||
habit.color = PaletteColor(1)
|
||||
saveIfSQLite(habit)
|
||||
@@ -86,11 +88,11 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis
|
||||
|
||||
fun createLongNumericalHabit(reference: Timestamp): Habit {
|
||||
val habit = modelFactory.buildHabit()
|
||||
habit.type = Habit.NUMBER_HABIT
|
||||
habit.type = HabitType.NUMERICAL
|
||||
habit.name = "Walk"
|
||||
habit.question = "How many steps did you walk today?"
|
||||
habit.unit = "steps"
|
||||
habit.targetType = Habit.AT_LEAST
|
||||
habit.targetType = NumericalHabitType.AT_LEAST
|
||||
habit.targetValue = 100.0
|
||||
habit.color = PaletteColor(1)
|
||||
saveIfSQLite(habit)
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.isoron.uhabits.core.AppScope
|
||||
import org.isoron.uhabits.core.commands.Command
|
||||
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.HabitList
|
||||
import org.isoron.uhabits.core.models.HabitList.Order
|
||||
@@ -54,8 +55,12 @@ import javax.inject.Inject
|
||||
class HabitCardListCache @Inject constructor(
|
||||
private val allHabits: HabitList,
|
||||
private val commandRunner: CommandRunner,
|
||||
taskRunner: TaskRunner
|
||||
taskRunner: TaskRunner,
|
||||
logging: Logging,
|
||||
) : CommandRunner.Listener {
|
||||
|
||||
private val logger = logging.getLogger("HabitCardListCache")
|
||||
|
||||
private var checkmarkCount = 0
|
||||
private var currentFetchTask: Task? = null
|
||||
private var listener: Listener
|
||||
@@ -316,8 +321,17 @@ class HabitCardListCache @Inject constructor(
|
||||
toPosition: Int
|
||||
) {
|
||||
data.habits.removeAt(fromPosition)
|
||||
data.habits.add(toPosition, habit)
|
||||
listener.onItemMoved(fromPosition, toPosition)
|
||||
|
||||
// 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}")
|
||||
data.habits.size
|
||||
} else {
|
||||
toPosition
|
||||
}
|
||||
|
||||
data.habits.add(checkedToPosition, habit)
|
||||
listener.onItemMoved(fromPosition, checkedToPosition)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
||||
@@ -57,6 +57,7 @@ data class ShowHabitState(
|
||||
val frequency: FrequencyCardState,
|
||||
val history: HistoryCardState,
|
||||
val bar: BarCardState,
|
||||
val theme: Theme,
|
||||
)
|
||||
|
||||
class ShowHabitPresenter(
|
||||
@@ -94,11 +95,14 @@ class ShowHabitPresenter(
|
||||
title = habit.name,
|
||||
color = habit.color,
|
||||
isNumerical = habit.isNumerical,
|
||||
theme = theme,
|
||||
subtitle = SubtitleCardPresenter.buildState(
|
||||
habit = habit,
|
||||
theme = theme,
|
||||
),
|
||||
overview = OverviewCardPresenter.buildState(
|
||||
habit = habit,
|
||||
theme = theme,
|
||||
),
|
||||
notes = NotesCardPresenter.buildState(
|
||||
habit = habit,
|
||||
@@ -106,18 +110,22 @@ class ShowHabitPresenter(
|
||||
target = TargetCardPresenter.buildState(
|
||||
habit = habit,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
theme = theme,
|
||||
),
|
||||
streaks = StreakCartPresenter.buildState(
|
||||
habit = habit,
|
||||
theme = theme,
|
||||
),
|
||||
scores = ScoreCardPresenter.buildState(
|
||||
spinnerPosition = preferences.scoreCardSpinnerPosition,
|
||||
habit = habit,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
theme = theme,
|
||||
),
|
||||
frequency = FrequencyCardPresenter.buildState(
|
||||
habit = habit,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
theme = theme,
|
||||
),
|
||||
history = HistoryCardPresenter.buildState(
|
||||
habit = habit,
|
||||
|
||||
@@ -22,12 +22,14 @@ 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.ui.views.Theme
|
||||
import java.util.HashMap
|
||||
|
||||
data class FrequencyCardState(
|
||||
val color: PaletteColor,
|
||||
val firstWeekday: Int,
|
||||
val frequency: HashMap<Timestamp, Array<Int>>,
|
||||
val theme: Theme,
|
||||
)
|
||||
|
||||
class FrequencyCardPresenter {
|
||||
@@ -35,12 +37,14 @@ class FrequencyCardPresenter {
|
||||
fun buildState(
|
||||
habit: Habit,
|
||||
firstWeekday: Int,
|
||||
theme: Theme
|
||||
) = FrequencyCardState(
|
||||
color = habit.color,
|
||||
frequency = habit.originalEntries.computeWeekdayFrequency(
|
||||
isNumerical = habit.isNumerical
|
||||
),
|
||||
firstWeekday = firstWeekday,
|
||||
theme = theme,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,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.PaletteColor
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
|
||||
data class OverviewCardState(
|
||||
@@ -30,11 +31,12 @@ data class OverviewCardState(
|
||||
val scoreYearDiff: Float,
|
||||
val scoreToday: Float,
|
||||
val totalCount: Long,
|
||||
val theme: Theme,
|
||||
)
|
||||
|
||||
class OverviewCardPresenter {
|
||||
companion object {
|
||||
fun buildState(habit: Habit): OverviewCardState {
|
||||
fun buildState(habit: Habit, theme: Theme): OverviewCardState {
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
val lastMonth = today.minus(30)
|
||||
val lastYear = today.minus(365)
|
||||
@@ -52,6 +54,7 @@ class OverviewCardPresenter {
|
||||
scoreMonthDiff = scoreToday - scoreLastMonth,
|
||||
scoreYearDiff = scoreToday - scoreLastYear,
|
||||
totalCount = totalCount,
|
||||
theme = theme,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.Score
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
|
||||
data class ScoreCardState(
|
||||
@@ -30,6 +31,7 @@ data class ScoreCardState(
|
||||
val bucketSize: Int,
|
||||
val spinnerPosition: Int,
|
||||
val color: PaletteColor,
|
||||
val theme: Theme,
|
||||
)
|
||||
|
||||
class ScoreCardPresenter(
|
||||
@@ -53,6 +55,7 @@ class ScoreCardPresenter(
|
||||
habit: Habit,
|
||||
firstWeekday: Int,
|
||||
spinnerPosition: Int,
|
||||
theme: Theme,
|
||||
): ScoreCardState {
|
||||
val bucketSize = BUCKET_SIZES[spinnerPosition]
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
@@ -77,6 +80,7 @@ class ScoreCardPresenter(
|
||||
scores = scores,
|
||||
bucketSize = bucketSize,
|
||||
spinnerPosition = spinnerPosition,
|
||||
theme = theme,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,18 +22,21 @@ 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.Streak
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
|
||||
data class StreakCardState(
|
||||
val color: PaletteColor,
|
||||
val bestStreaks: List<Streak>
|
||||
val bestStreaks: List<Streak>,
|
||||
val theme: Theme,
|
||||
)
|
||||
|
||||
class StreakCartPresenter {
|
||||
companion object {
|
||||
fun buildState(habit: Habit): StreakCardState {
|
||||
fun buildState(habit: Habit, theme: Theme): StreakCardState {
|
||||
return StreakCardState(
|
||||
color = habit.color,
|
||||
bestStreaks = habit.streaks.getBest(10),
|
||||
theme = theme,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.Reminder
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
|
||||
data class SubtitleCardState(
|
||||
val color: PaletteColor,
|
||||
@@ -32,12 +33,14 @@ data class SubtitleCardState(
|
||||
val reminder: Reminder?,
|
||||
val targetValue: Double,
|
||||
val unit: String,
|
||||
val theme: Theme,
|
||||
)
|
||||
|
||||
class SubtitleCardPresenter {
|
||||
companion object {
|
||||
fun buildState(
|
||||
habit: Habit,
|
||||
theme: Theme,
|
||||
): SubtitleCardState = SubtitleCardState(
|
||||
color = habit.color,
|
||||
frequency = habit.frequency,
|
||||
@@ -46,6 +49,7 @@ class SubtitleCardPresenter {
|
||||
reminder = habit.reminder,
|
||||
targetValue = habit.targetValue,
|
||||
unit = habit.unit,
|
||||
theme = theme,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,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.groupedSum
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import java.util.ArrayList
|
||||
import java.util.Calendar
|
||||
@@ -31,6 +32,7 @@ data class TargetCardState(
|
||||
val values: List<Double> = listOf(),
|
||||
val targets: List<Double> = listOf(),
|
||||
val intervals: List<Int> = listOf(),
|
||||
val theme: Theme,
|
||||
)
|
||||
|
||||
class TargetCardPresenter {
|
||||
@@ -38,6 +40,7 @@ class TargetCardPresenter {
|
||||
fun buildState(
|
||||
habit: Habit,
|
||||
firstWeekday: Int,
|
||||
theme: Theme,
|
||||
): TargetCardState {
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
|
||||
@@ -106,6 +109,7 @@ class TargetCardPresenter {
|
||||
values = values,
|
||||
targets = targets,
|
||||
intervals = intervals,
|
||||
theme = theme,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ class BarChart(
|
||||
canvas.drawLine(0.0, y, width, y)
|
||||
canvas.setColor(theme.mediumContrastTextColor)
|
||||
canvas.setTextAlign(TextAlign.CENTER)
|
||||
canvas.setFontSize(theme.smallTextSize)
|
||||
var prevMonth = -1
|
||||
var prevYear = -1
|
||||
val isLargeInterval = axis.size < 2 || (axis[0].distanceTo(axis[1]) > 300)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package org.isoron.uhabits.core.ui.views
|
||||
|
||||
import org.isoron.platform.gui.Color
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
|
||||
abstract class Theme {
|
||||
open val appBackgroundColor = Color(0xf4f4f4)
|
||||
@@ -35,6 +36,10 @@ abstract class Theme {
|
||||
open val toolbarBackgroundColor = Color(0xf4f4f4)
|
||||
open val toolbarColor = Color(0xffffff)
|
||||
|
||||
fun color(paletteColor: PaletteColor): Color {
|
||||
return color(paletteColor.paletteIndex)
|
||||
}
|
||||
|
||||
open fun color(paletteIndex: Int): Color {
|
||||
return when (paletteIndex) {
|
||||
0 -> Color(0xD32F2F)
|
||||
@@ -109,6 +114,12 @@ open class DarkTheme : Theme() {
|
||||
}
|
||||
}
|
||||
|
||||
class PureBlackTheme : DarkTheme() {
|
||||
override val appBackgroundColor = Color(0x000000)
|
||||
override val cardBackgroundColor = Color(0x000000)
|
||||
override val lowContrastTextColor = Color(0x212121)
|
||||
}
|
||||
|
||||
class WidgetTheme : LightTheme() {
|
||||
override val cardBackgroundColor = Color.TRANSPARENT
|
||||
override val highContrastTextColor = Color.WHITE
|
||||
|
||||
@@ -46,8 +46,8 @@ class WidgetBehavior @Inject constructor(
|
||||
setValue(habit, timestamp, Entry.NO)
|
||||
}
|
||||
|
||||
fun onToggleRepetition(habit: Habit, timestamp: Timestamp?) {
|
||||
val currentValue = habit.originalEntries.get(timestamp!!).value
|
||||
fun onToggleRepetition(habit: Habit, timestamp: Timestamp) {
|
||||
val currentValue = habit.originalEntries.get(timestamp).value
|
||||
val newValue: Int
|
||||
newValue =
|
||||
if (preferences.isSkipEnabled) nextToggleValueWithSkip(
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
import org.isoron.platform.time.LocalDate
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class DatesTest {
|
||||
@Test
|
||||
fun testDatesBefore2000() {
|
||||
val date = LocalDate(-1)
|
||||
assertEquals(date.day, 31)
|
||||
assertEquals(date.month, 12)
|
||||
assertEquals(date.year, 1999)
|
||||
}
|
||||
}
|
||||
@@ -80,12 +80,22 @@ class HabitTest : BaseUnitTest() {
|
||||
assertTrue(h.isCompletedToday())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun test_isFailed() {
|
||||
val h = modelFactory.buildHabit()
|
||||
assertFalse(h.isFailedToday())
|
||||
h.originalEntries.add(Entry(getToday(), Entry.NO))
|
||||
h.recompute()
|
||||
assertTrue(h.isFailedToday())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun test_isCompleted_numerical() {
|
||||
val h = modelFactory.buildHabit()
|
||||
h.type = Habit.NUMBER_HABIT
|
||||
h.targetType = Habit.AT_LEAST
|
||||
h.type = HabitType.NUMERICAL
|
||||
h.targetType = NumericalHabitType.AT_LEAST
|
||||
h.targetValue = 100.0
|
||||
assertFalse(h.isCompletedToday())
|
||||
h.originalEntries.add(Entry(getToday(), 200000))
|
||||
@@ -97,7 +107,7 @@ class HabitTest : BaseUnitTest() {
|
||||
h.originalEntries.add(Entry(getToday(), 50000))
|
||||
h.recompute()
|
||||
assertFalse(h.isCompletedToday())
|
||||
h.targetType = Habit.AT_MOST
|
||||
h.targetType = NumericalHabitType.AT_MOST
|
||||
h.originalEntries.add(Entry(getToday(), 200000))
|
||||
h.recompute()
|
||||
assertFalse(h.isCompletedToday())
|
||||
@@ -109,6 +119,35 @@ class HabitTest : BaseUnitTest() {
|
||||
assertTrue(h.isCompletedToday())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun test_isFailedNumerical() {
|
||||
val h = modelFactory.buildHabit()
|
||||
h.type = HabitType.NUMERICAL
|
||||
h.targetType = NumericalHabitType.AT_LEAST
|
||||
h.targetValue = 100.0
|
||||
assertTrue(h.isFailedToday())
|
||||
h.originalEntries.add(Entry(getToday(), 200000))
|
||||
h.recompute()
|
||||
assertFalse(h.isFailedToday())
|
||||
h.originalEntries.add(Entry(getToday(), 100000))
|
||||
h.recompute()
|
||||
assertFalse(h.isFailedToday())
|
||||
h.originalEntries.add(Entry(getToday(), 50000))
|
||||
h.recompute()
|
||||
assertTrue(h.isFailedToday())
|
||||
h.targetType = NumericalHabitType.AT_MOST
|
||||
h.originalEntries.add(Entry(getToday(), 200000))
|
||||
h.recompute()
|
||||
assertTrue(h.isFailedToday())
|
||||
h.originalEntries.add(Entry(getToday(), 100000))
|
||||
h.recompute()
|
||||
assertFalse(h.isFailedToday())
|
||||
h.originalEntries.add(Entry(getToday(), 50000))
|
||||
h.recompute()
|
||||
assertFalse(h.isFailedToday())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testURI() {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.isoron.uhabits.core.models
|
||||
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.number.IsCloseTo
|
||||
import org.hamcrest.number.OrderingComparison
|
||||
@@ -121,6 +122,14 @@ class ScoreListTest : BaseUnitTest() {
|
||||
checkScoreValues(expectedValues)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_withZeroTarget() {
|
||||
habit = fixtures.createNumericalHabit()
|
||||
habit.targetValue = 0.0
|
||||
habit.recompute()
|
||||
assertTrue(habit.scores[today].value.isFinite())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_imperfectNonDaily() {
|
||||
// If the habit should be performed 3 times per week and the user misses 1 repetition
|
||||
|
||||
@@ -22,7 +22,8 @@ import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.isoron.uhabits.core.BaseUnitTest
|
||||
import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
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.WeekdayList
|
||||
@@ -59,9 +60,9 @@ class HabitRecordTest : BaseUnitTest() {
|
||||
reminder = null
|
||||
id = 1L
|
||||
position = 15
|
||||
type = Habit.NUMBER_HABIT
|
||||
type = HabitType.NUMERICAL
|
||||
targetValue = 100.0
|
||||
targetType = Habit.AT_LEAST
|
||||
targetType = NumericalHabitType.AT_LEAST
|
||||
unit = "miles"
|
||||
}
|
||||
val record = HabitRecord()
|
||||
|
||||
@@ -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)
|
||||
cache = HabitCardListCache(habitList, commandRunner, taskRunner, mock())
|
||||
cache.setCheckmarkCount(10)
|
||||
cache.refreshAllHabits()
|
||||
cache.onAttached()
|
||||
|
||||
Reference in New Issue
Block a user