first commit
This commit is contained in:
parent
4383b3ed03
commit
23f9c45329
3
gradle/gradle-daemon-jvm.properties
Normal file
3
gradle/gradle-daemon-jvm.properties
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#This file is generated by updateDaemonJvm
|
||||||
|
toolchainVendor=jetbrains
|
||||||
|
toolchainVersion=21
|
||||||
@ -10,6 +10,9 @@ pluginManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0"
|
||||||
|
}
|
||||||
|
|
||||||
include(":uhabits-android", ":uhabits-core")
|
include(":uhabits-android", ":uhabits-core")
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import org.isoron.uhabits.activities.habits.list.ListHabitsModule
|
|||||||
import org.isoron.uhabits.activities.habits.list.ListHabitsScreen
|
import org.isoron.uhabits.activities.habits.list.ListHabitsScreen
|
||||||
import org.isoron.uhabits.activities.habits.list.views.CheckmarkButtonViewFactory
|
import org.isoron.uhabits.activities.habits.list.views.CheckmarkButtonViewFactory
|
||||||
import org.isoron.uhabits.activities.habits.list.views.CheckmarkPanelViewFactory
|
import org.isoron.uhabits.activities.habits.list.views.CheckmarkPanelViewFactory
|
||||||
|
import org.isoron.uhabits.activities.habits.list.views.CounterPanelViewFactory
|
||||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
||||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardViewFactory
|
import org.isoron.uhabits.activities.habits.list.views.HabitCardViewFactory
|
||||||
import org.isoron.uhabits.activities.habits.list.views.NumberButtonViewFactory
|
import org.isoron.uhabits.activities.habits.list.views.NumberButtonViewFactory
|
||||||
@ -48,6 +49,7 @@ abstract class HabitsActivityTestComponent(
|
|||||||
) {
|
) {
|
||||||
abstract fun getCheckmarkPanelViewFactory(): CheckmarkPanelViewFactory
|
abstract fun getCheckmarkPanelViewFactory(): CheckmarkPanelViewFactory
|
||||||
abstract fun getHabitCardViewFactory(): HabitCardViewFactory
|
abstract fun getHabitCardViewFactory(): HabitCardViewFactory
|
||||||
|
abstract fun getCounterPanelViewFactory(): CounterPanelViewFactory
|
||||||
abstract fun getEntryButtonViewFactory(): CheckmarkButtonViewFactory
|
abstract fun getEntryButtonViewFactory(): CheckmarkButtonViewFactory
|
||||||
abstract fun getNumberButtonViewFactory(): NumberButtonViewFactory
|
abstract fun getNumberButtonViewFactory(): NumberButtonViewFactory
|
||||||
abstract fun getNumberPanelViewFactory(): NumberPanelViewFactory
|
abstract fun getNumberPanelViewFactory(): NumberPanelViewFactory
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.InputType
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.format.DateFormat
|
import android.text.format.DateFormat
|
||||||
@ -58,6 +59,20 @@ import org.isoron.uhabits.utils.dismissCurrentAndShow
|
|||||||
import org.isoron.uhabits.utils.formatTime
|
import org.isoron.uhabits.utils.formatTime
|
||||||
import org.isoron.uhabits.utils.toFormattedString
|
import org.isoron.uhabits.utils.toFormattedString
|
||||||
|
|
||||||
|
private data class UnitPreset(
|
||||||
|
val label: String,
|
||||||
|
val unit: String,
|
||||||
|
val minutesPerUnit: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
private val TIME_COST_PRESETS = listOf(
|
||||||
|
UnitPreset("Банка (500 мл)", "банка", 30),
|
||||||
|
UnitPreset("Стакан (250 мл)", "стакан", 15),
|
||||||
|
UnitPreset("Глоток", "глоток", 1),
|
||||||
|
UnitPreset("Сигарета", "сигарета", 11),
|
||||||
|
UnitPreset("Затяжка", "затяжка", 1)
|
||||||
|
)
|
||||||
|
|
||||||
fun formatFrequency(freqNum: Int, freqDen: Int, resources: Resources) = when {
|
fun formatFrequency(freqNum: Int, freqDen: Int, resources: Resources) = when {
|
||||||
freqNum == 1 && (freqDen == 30 || freqDen == 31) -> resources.getString(R.string.every_month)
|
freqNum == 1 && (freqDen == 30 || freqDen == 31) -> resources.getString(R.string.every_month)
|
||||||
freqDen == 30 || freqDen == 31 -> resources.getString(R.string.x_times_per_month, freqNum)
|
freqDen == 30 || freqDen == 31 -> resources.getString(R.string.x_times_per_month, freqNum)
|
||||||
@ -77,6 +92,7 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
var habitId = -1L
|
var habitId = -1L
|
||||||
lateinit var habitType: HabitType
|
lateinit var habitType: HabitType
|
||||||
var unit = ""
|
var unit = ""
|
||||||
|
var minutesPerUnit = 0
|
||||||
var color = PaletteColor(11)
|
var color = PaletteColor(11)
|
||||||
var androidColor = 0
|
var androidColor = 0
|
||||||
var freqNum = 1
|
var freqNum = 1
|
||||||
@ -107,6 +123,8 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
freqNum = habit.frequency.numerator
|
freqNum = habit.frequency.numerator
|
||||||
freqDen = habit.frequency.denominator
|
freqDen = habit.frequency.denominator
|
||||||
targetType = habit.targetType
|
targetType = habit.targetType
|
||||||
|
unit = habit.unit
|
||||||
|
minutesPerUnit = habit.minutesPerUnit
|
||||||
habit.reminder?.let {
|
habit.reminder?.let {
|
||||||
reminderHour = it.hour
|
reminderHour = it.hour
|
||||||
reminderMin = it.minute
|
reminderMin = it.minute
|
||||||
@ -127,12 +145,15 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
color = PaletteColor(state.getInt("paletteColor"))
|
color = PaletteColor(state.getInt("paletteColor"))
|
||||||
freqNum = state.getInt("freqNum")
|
freqNum = state.getInt("freqNum")
|
||||||
freqDen = state.getInt("freqDen")
|
freqDen = state.getInt("freqDen")
|
||||||
|
unit = state.getString("unit") ?: unit
|
||||||
|
minutesPerUnit = state.getInt("minutesPerUnit")
|
||||||
reminderHour = state.getInt("reminderHour")
|
reminderHour = state.getInt("reminderHour")
|
||||||
reminderMin = state.getInt("reminderMin")
|
reminderMin = state.getInt("reminderMin")
|
||||||
reminderDays = WeekdayList(state.getInt("reminderDays"))
|
reminderDays = WeekdayList(state.getInt("reminderDays"))
|
||||||
}
|
}
|
||||||
|
|
||||||
updateColors()
|
updateColors()
|
||||||
|
setupUnitDropdown()
|
||||||
|
|
||||||
when (habitType) {
|
when (habitType) {
|
||||||
HabitType.YES_NO -> {
|
HabitType.YES_NO -> {
|
||||||
@ -145,6 +166,19 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
binding.questionInput.hint = getString(R.string.measurable_question_example)
|
binding.questionInput.hint = getString(R.string.measurable_question_example)
|
||||||
binding.frequencyOuterBox.visibility = View.GONE
|
binding.frequencyOuterBox.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
HabitType.TIME_COST -> {
|
||||||
|
binding.nameInput.hint = getString(R.string.time_cost_short_example)
|
||||||
|
binding.questionInput.hint = getString(R.string.time_cost_question_example)
|
||||||
|
binding.frequencyOuterBox.visibility = View.GONE
|
||||||
|
binding.targetOuterBox.visibility = View.GONE
|
||||||
|
binding.targetTypeOuterBox.visibility = View.GONE
|
||||||
|
val preset = findPresetByUnit(unit)
|
||||||
|
?: findPresetByMinutes(minutesPerUnit)
|
||||||
|
?: TIME_COST_PRESETS.first()
|
||||||
|
minutesPerUnit = preset.minutesPerUnit
|
||||||
|
unit = preset.unit
|
||||||
|
binding.unitInput.setText(preset.label, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
@ -283,6 +317,10 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
habit.targetValue = binding.targetInput.text.toString().toDouble()
|
habit.targetValue = binding.targetInput.text.toString().toDouble()
|
||||||
habit.targetType = targetType
|
habit.targetType = targetType
|
||||||
habit.unit = binding.unitInput.text.trim().toString()
|
habit.unit = binding.unitInput.text.trim().toString()
|
||||||
|
} else if (habitType == HabitType.TIME_COST) {
|
||||||
|
val preset = selectedPreset() ?: findPresetByUnit(unit) ?: TIME_COST_PRESETS.first()
|
||||||
|
habit.unit = preset.unit
|
||||||
|
habit.minutesPerUnit = preset.minutesPerUnit
|
||||||
}
|
}
|
||||||
habit.type = habitType
|
habit.type = habitType
|
||||||
|
|
||||||
@ -314,10 +352,67 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
binding.targetInput.error = getString(R.string.validation_cannot_be_blank)
|
binding.targetInput.error = getString(R.string.validation_cannot_be_blank)
|
||||||
isValid = false
|
isValid = false
|
||||||
}
|
}
|
||||||
|
} else if (habitType == HabitType.TIME_COST) {
|
||||||
|
if (binding.unitInput.text.isNullOrEmpty()) {
|
||||||
|
binding.unitInput.error = getString(R.string.validation_cannot_be_blank)
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return isValid
|
return isValid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupUnitDropdown() {
|
||||||
|
if (habitType == HabitType.TIME_COST) {
|
||||||
|
val adapter = ArrayAdapter(
|
||||||
|
this,
|
||||||
|
android.R.layout.simple_list_item_1,
|
||||||
|
TIME_COST_PRESETS.map { it.label }
|
||||||
|
)
|
||||||
|
binding.unitInput.setAdapter(adapter)
|
||||||
|
binding.unitInput.setOnItemClickListener { _, _, position, _ ->
|
||||||
|
val preset = TIME_COST_PRESETS[position]
|
||||||
|
minutesPerUnit = preset.minutesPerUnit
|
||||||
|
binding.unitInput.setText(preset.label, false)
|
||||||
|
}
|
||||||
|
binding.unitInput.setOnFocusChangeListener { _, hasFocus ->
|
||||||
|
if (!hasFocus) syncPresetFromText()
|
||||||
|
}
|
||||||
|
binding.unitInput.setOnClickListener {
|
||||||
|
binding.unitInput.showDropDown()
|
||||||
|
}
|
||||||
|
binding.unitInput.inputType = InputType.TYPE_NULL
|
||||||
|
binding.unitInput.isFocusable = false
|
||||||
|
binding.unitInput.isClickable = true
|
||||||
|
} else {
|
||||||
|
binding.unitInput.setOnItemClickListener(null)
|
||||||
|
binding.unitInput.setOnFocusChangeListener(null)
|
||||||
|
binding.unitInput.setOnClickListener(null)
|
||||||
|
binding.unitInput.inputType = InputType.TYPE_CLASS_TEXT
|
||||||
|
binding.unitInput.isFocusable = true
|
||||||
|
binding.unitInput.isClickable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectedPreset(): UnitPreset? {
|
||||||
|
val value = binding.unitInput.text?.toString()?.trim().orEmpty()
|
||||||
|
return TIME_COST_PRESETS.firstOrNull { it.label == value }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findPresetByMinutes(minutes: Int): UnitPreset? {
|
||||||
|
return TIME_COST_PRESETS.firstOrNull { it.minutesPerUnit == minutes }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findPresetByUnit(unit: String): UnitPreset? {
|
||||||
|
return TIME_COST_PRESETS.firstOrNull { it.unit == unit || it.label == unit }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncPresetFromText() {
|
||||||
|
selectedPreset()?.let { preset ->
|
||||||
|
minutesPerUnit = preset.minutesPerUnit
|
||||||
|
binding.unitInput.setText(preset.label, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun populateReminder() {
|
private fun populateReminder() {
|
||||||
if (reminderHour < 0) {
|
if (reminderHour < 0) {
|
||||||
binding.reminderTimePicker.text = getString(R.string.reminder_off)
|
binding.reminderTimePicker.text = getString(R.string.reminder_off)
|
||||||
@ -373,6 +468,8 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
putInt("androidColor", androidColor)
|
putInt("androidColor", androidColor)
|
||||||
putInt("freqNum", freqNum)
|
putInt("freqNum", freqNum)
|
||||||
putInt("freqDen", freqDen)
|
putInt("freqDen", freqDen)
|
||||||
|
putString("unit", unit)
|
||||||
|
putInt("minutesPerUnit", minutesPerUnit)
|
||||||
putInt("reminderHour", reminderHour)
|
putInt("reminderHour", reminderHour)
|
||||||
putInt("reminderMin", reminderMin)
|
putInt("reminderMin", reminderMin)
|
||||||
putInt("reminderDays", reminderDays.toInteger())
|
putInt("reminderDays", reminderDays.toInteger())
|
||||||
|
|||||||
@ -51,6 +51,12 @@ class HabitTypeDialog : AppCompatDialogFragment() {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.buttonTimeCost.setOnClickListener {
|
||||||
|
val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.TIME_COST.value)
|
||||||
|
startActivity(intent)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
binding.background.setOnClickListener {
|
binding.background.setOnClickListener {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2025 Á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.uhabits.activities.habits.list.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
|
import me.tatarka.inject.annotations.Inject
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
|
import org.isoron.uhabits.inject.ActivityContext
|
||||||
|
import org.isoron.uhabits.utils.dp
|
||||||
|
import org.isoron.uhabits.utils.sres
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
class CounterPanelViewFactory(
|
||||||
|
@ActivityContext val context: Context,
|
||||||
|
val preferences: Preferences
|
||||||
|
) {
|
||||||
|
fun create() = CounterPanelView(context, preferences)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CounterPanelView(
|
||||||
|
context: Context,
|
||||||
|
private val preferences: Preferences
|
||||||
|
) : LinearLayout(context), Preferences.Listener {
|
||||||
|
|
||||||
|
var value: Int = 0
|
||||||
|
set(newValue) {
|
||||||
|
field = newValue.coerceAtLeast(0)
|
||||||
|
countView.text = field.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
var color: Int = 0
|
||||||
|
set(newValue) {
|
||||||
|
field = newValue
|
||||||
|
refreshColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
var onEdit: () -> Unit = {}
|
||||||
|
|
||||||
|
var onChange: (Int) -> Unit = {}
|
||||||
|
|
||||||
|
private val minusButton = buildButton("-")
|
||||||
|
private val countView = buildCountView()
|
||||||
|
private val plusButton = buildButton("+")
|
||||||
|
|
||||||
|
init {
|
||||||
|
orientation = HORIZONTAL
|
||||||
|
layoutParams = LayoutParams(MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||||
|
addView(minusButton)
|
||||||
|
addView(countView)
|
||||||
|
addView(plusButton)
|
||||||
|
value = 0
|
||||||
|
setupActions()
|
||||||
|
refreshColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
preferences.addListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
preferences.removeListener(this)
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCheckmarkSequenceChanged() {
|
||||||
|
refreshColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupActions() {
|
||||||
|
minusButton.setOnClickListener {
|
||||||
|
updateValue(value - 1)
|
||||||
|
}
|
||||||
|
plusButton.setOnClickListener {
|
||||||
|
updateValue(value + 1)
|
||||||
|
}
|
||||||
|
listOf(minusButton, countView, plusButton).forEach { view ->
|
||||||
|
view.setOnLongClickListener {
|
||||||
|
onEdit()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
countView.setOnClickListener {
|
||||||
|
onEdit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateValue(newValue: Int) {
|
||||||
|
if (newValue == value) return
|
||||||
|
value = newValue
|
||||||
|
onChange(value)
|
||||||
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildButton(text: String): AppCompatTextView {
|
||||||
|
return AppCompatTextView(context).apply {
|
||||||
|
layoutParams = LayoutParams(0, dp(40f).toInt(), 1f)
|
||||||
|
this.text = text
|
||||||
|
gravity = android.view.Gravity.CENTER
|
||||||
|
isClickable = true
|
||||||
|
isFocusable = true
|
||||||
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 22f)
|
||||||
|
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||||
|
setBackgroundResource(R.drawable.round_ripple)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildCountView(): AppCompatTextView {
|
||||||
|
return AppCompatTextView(context).apply {
|
||||||
|
layoutParams = LayoutParams(0, dp(40f).toInt(), 1.35f)
|
||||||
|
gravity = android.view.Gravity.CENTER
|
||||||
|
isClickable = true
|
||||||
|
isFocusable = true
|
||||||
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
|
||||||
|
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||||
|
setBackgroundResource(R.drawable.bg_input_box)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshColors() {
|
||||||
|
val activeColor = if (color == 0) sres.getColor(R.attr.contrast60) else color
|
||||||
|
minusButton.setTextColor(activeColor)
|
||||||
|
plusButton.setTextColor(activeColor)
|
||||||
|
countView.setTextColor(activeColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,8 @@ import android.os.Looper
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.View.GONE
|
||||||
|
import android.view.View.VISIBLE
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
@ -52,15 +54,23 @@ import org.isoron.uhabits.utils.sres
|
|||||||
class HabitCardViewFactory(
|
class HabitCardViewFactory(
|
||||||
@ActivityContext val context: Context,
|
@ActivityContext val context: Context,
|
||||||
private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
||||||
|
private val counterPanelFactory: CounterPanelViewFactory,
|
||||||
private val numberPanelFactory: NumberPanelViewFactory,
|
private val numberPanelFactory: NumberPanelViewFactory,
|
||||||
private val behavior: ListHabitsBehavior
|
private val behavior: ListHabitsBehavior
|
||||||
) {
|
) {
|
||||||
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
|
fun create() = HabitCardView(
|
||||||
|
context,
|
||||||
|
checkmarkPanelFactory,
|
||||||
|
counterPanelFactory,
|
||||||
|
numberPanelFactory,
|
||||||
|
behavior
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class HabitCardView(
|
class HabitCardView(
|
||||||
@ActivityContext context: Context,
|
@ActivityContext context: Context,
|
||||||
checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
||||||
|
counterPanelFactory: CounterPanelViewFactory,
|
||||||
numberPanelFactory: NumberPanelViewFactory,
|
numberPanelFactory: NumberPanelViewFactory,
|
||||||
private val behavior: ListHabitsBehavior
|
private val behavior: ListHabitsBehavior
|
||||||
) : FrameLayout(context),
|
) : FrameLayout(context),
|
||||||
@ -124,9 +134,12 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var checkmarkPanel: CheckmarkPanelView
|
var checkmarkPanel: CheckmarkPanelView
|
||||||
|
private var counterPanel: CounterPanelView
|
||||||
private var numberPanel: NumberPanelView
|
private var numberPanel: NumberPanelView
|
||||||
private var innerFrame: LinearLayout
|
private var innerFrame: LinearLayout
|
||||||
|
private var labelContainer: LinearLayout
|
||||||
private var label: TextView
|
private var label: TextView
|
||||||
|
private var lossText: TextView
|
||||||
private var scoreRing: RingView
|
private var scoreRing: RingView
|
||||||
|
|
||||||
private var currentToggleTaskId = 0
|
private var currentToggleTaskId = 0
|
||||||
@ -146,12 +159,25 @@ class HabitCardView(
|
|||||||
label = TextView(context).apply {
|
label = TextView(context).apply {
|
||||||
maxLines = 2
|
maxLines = 2
|
||||||
ellipsize = TextUtils.TruncateAt.END
|
ellipsize = TextUtils.TruncateAt.END
|
||||||
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
|
|
||||||
if (SDK_INT >= Build.VERSION_CODES.Q) {
|
if (SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
breakStrategy = BREAK_STRATEGY_BALANCED
|
breakStrategy = BREAK_STRATEGY_BALANCED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lossText = TextView(context).apply {
|
||||||
|
maxLines = 1
|
||||||
|
visibility = GONE
|
||||||
|
textSize = 12f
|
||||||
|
setTextColor(sres.getColor(R.attr.contrast60))
|
||||||
|
}
|
||||||
|
|
||||||
|
labelContainer = LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
|
||||||
|
addView(label)
|
||||||
|
addView(lossText)
|
||||||
|
}
|
||||||
|
|
||||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||||
onToggle = { date, value, notes ->
|
onToggle = { date, value, notes ->
|
||||||
triggerRipple(date)
|
triggerRipple(date)
|
||||||
@ -174,6 +200,29 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
counterPanel = counterPanelFactory.create().apply {
|
||||||
|
visibility = GONE
|
||||||
|
onChange = { value ->
|
||||||
|
val date = getToday()
|
||||||
|
val location = getAbsoluteCounterLocation()
|
||||||
|
habit?.let {
|
||||||
|
behavior.onToggle(
|
||||||
|
it,
|
||||||
|
date,
|
||||||
|
value,
|
||||||
|
it.computedEntries.get(date).notes,
|
||||||
|
location.x,
|
||||||
|
location.y
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onEdit = {
|
||||||
|
val date = getToday()
|
||||||
|
val location = getAbsoluteCounterLocation()
|
||||||
|
habit?.let { behavior.onEdit(it, date, location.x, location.y) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
numberPanel = numberPanelFactory.create().apply {
|
numberPanel = numberPanelFactory.create().apply {
|
||||||
visibility = GONE
|
visibility = GONE
|
||||||
onEdit = { date ->
|
onEdit = { date ->
|
||||||
@ -190,8 +239,9 @@ class HabitCardView(
|
|||||||
elevation = dp(1f)
|
elevation = dp(1f)
|
||||||
|
|
||||||
addView(scoreRing)
|
addView(scoreRing)
|
||||||
addView(label)
|
addView(labelContainer)
|
||||||
addView(checkmarkPanel)
|
addView(checkmarkPanel)
|
||||||
|
addView(counterPanel)
|
||||||
addView(numberPanel)
|
addView(numberPanel)
|
||||||
|
|
||||||
setOnTouchListener { v, event ->
|
setOnTouchListener { v, event ->
|
||||||
@ -253,6 +303,15 @@ class HabitCardView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getAbsoluteCounterLocation(): PointF {
|
||||||
|
val containerLocation = IntArray(2)
|
||||||
|
counterPanel.getLocationInWindow(containerLocation)
|
||||||
|
return PointF(
|
||||||
|
containerLocation[0].toFloat() + counterPanel.width / 2f,
|
||||||
|
containerLocation[1].toFloat() + counterPanel.height / 2f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
habit?.observable?.addListener(this)
|
habit?.observable?.addListener(this)
|
||||||
@ -276,25 +335,34 @@ class HabitCardView(
|
|||||||
text = h.name
|
text = h.name
|
||||||
setTextColor(c)
|
setTextColor(c)
|
||||||
}
|
}
|
||||||
|
lossText.apply {
|
||||||
|
visibility = if (h.isTimeCost) VISIBLE else GONE
|
||||||
|
text = if (h.isTimeCost) {
|
||||||
|
val lostMin = h.getTodayValue() * h.minutesPerUnit
|
||||||
|
context.getString(R.string.time_lost_today, lostMin)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
scoreRing.apply {
|
scoreRing.apply {
|
||||||
setColor(c)
|
setColor(c)
|
||||||
|
visibility = if (h.isNumerical && !h.isTimeCost) VISIBLE else GONE
|
||||||
}
|
}
|
||||||
checkmarkPanel.apply {
|
checkmarkPanel.apply {
|
||||||
color = c
|
color = c
|
||||||
visibility = when (h.isNumerical) {
|
visibility = if (h.isTimeCost || h.isNumerical) GONE else VISIBLE
|
||||||
true -> View.GONE
|
}
|
||||||
false -> View.VISIBLE
|
counterPanel.apply {
|
||||||
}
|
color = c
|
||||||
|
value = h.getTodayValue()
|
||||||
|
visibility = if (h.isTimeCost) VISIBLE else GONE
|
||||||
}
|
}
|
||||||
numberPanel.apply {
|
numberPanel.apply {
|
||||||
color = c
|
color = c
|
||||||
units = h.unit
|
units = h.unit
|
||||||
targetType = h.targetType
|
targetType = h.targetType
|
||||||
threshold = h.targetValue
|
threshold = h.targetValue
|
||||||
visibility = when (h.isNumerical) {
|
visibility = if (h.isNumerical && !h.isTimeCost) VISIBLE else GONE
|
||||||
true -> View.VISIBLE
|
|
||||||
false -> View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -163,11 +163,14 @@
|
|||||||
<TextView
|
<TextView
|
||||||
style="@style/FormLabel"
|
style="@style/FormLabel"
|
||||||
android:text="@string/unit" />
|
android:text="@string/unit" />
|
||||||
<EditText
|
<androidx.appcompat.widget.AppCompatAutoCompleteTextView
|
||||||
style="@style/FormInput"
|
style="@style/FormInput"
|
||||||
android:id="@+id/unitInput"
|
android:id="@+id/unitInput"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:ems="10"
|
android:ems="10"
|
||||||
|
android:inputType="none"
|
||||||
|
android:focusable="false"
|
||||||
|
android:clickable="true"
|
||||||
android:hint="@string/measurable_units_example"/>
|
android:hint="@string/measurable_units_example"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@ -67,6 +67,25 @@
|
|||||||
android:text="@string/measurable_example" />
|
android:text="@string/measurable_example" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/buttonTimeCost"
|
||||||
|
style="@style/SelectHabitTypeButton">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="@style/SelectHabitTypeButtonTitle"
|
||||||
|
android:text="@string/time_cost" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="@style/SelectHabitTypeButtonBody"
|
||||||
|
android:text="@string/time_cost_example" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- <LinearLayout-->
|
<!-- <LinearLayout-->
|
||||||
<!-- android:layout_width="match_parent"-->
|
<!-- android:layout_width="match_parent"-->
|
||||||
<!-- android:layout_height="wrap_content"-->
|
<!-- android:layout_height="wrap_content"-->
|
||||||
@ -86,4 +105,4 @@
|
|||||||
<!-- android:text="@string/subjective_example" />-->
|
<!-- android:text="@string/subjective_example" />-->
|
||||||
<!-- </LinearLayout>-->
|
<!-- </LinearLayout>-->
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@ -208,6 +208,8 @@
|
|||||||
<string name="yes_or_no_example">e.g. Did you wake up early today? Did you exercise? Did you play chess?</string>
|
<string name="yes_or_no_example">e.g. Did you wake up early today? Did you exercise? Did you play chess?</string>
|
||||||
<string name="measurable">Measurable</string>
|
<string name="measurable">Measurable</string>
|
||||||
<string name="measurable_example">e.g. How many miles did you run today? How many pages did you read?</string>
|
<string name="measurable_example">e.g. How many miles did you run today? How many pages did you read?</string>
|
||||||
|
<string name="time_cost">Time Cost</string>
|
||||||
|
<string name="time_cost_example">Track lost time in minutes</string>
|
||||||
<string name="x_times_per_week">%d times per week</string>
|
<string name="x_times_per_week">%d times per week</string>
|
||||||
<string name="x_times_per_month">%d times per month</string>
|
<string name="x_times_per_month">%d times per month</string>
|
||||||
<string name="x_times_per_y_days">%d times in %d days</string>
|
<string name="x_times_per_y_days">%d times in %d days</string>
|
||||||
@ -217,6 +219,9 @@
|
|||||||
<string name="measurable_short_example">e.g. Run</string>
|
<string name="measurable_short_example">e.g. Run</string>
|
||||||
<string name="measurable_question_example">e.g. How many miles did you run today?</string>
|
<string name="measurable_question_example">e.g. How many miles did you run today?</string>
|
||||||
<string name="measurable_units_example">e.g. miles</string>
|
<string name="measurable_units_example">e.g. miles</string>
|
||||||
|
<string name="time_cost_short_example">e.g. Smoke</string>
|
||||||
|
<string name="time_cost_question_example">e.g. How many cigarettes today?</string>
|
||||||
|
<string name="time_lost_today">Time lost today: %d min</string>
|
||||||
<string name="every_month">Every month</string>
|
<string name="every_month">Every month</string>
|
||||||
<string name="validation_cannot_be_blank">Cannot be blank</string>
|
<string name="validation_cannot_be_blank">Cannot be blank</string>
|
||||||
<string name="today">Today</string>
|
<string name="today">Today</string>
|
||||||
|
|||||||
1
uhabits-core/assets/main/migrations/26.sql
Normal file
1
uhabits-core/assets/main/migrations/26.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
alter table Habits add column minutes_per_unit integer not null default 0;
|
||||||
@ -20,4 +20,4 @@ package org.isoron.uhabits.core
|
|||||||
|
|
||||||
const val DATABASE_FILENAME = "uhabits.db"
|
const val DATABASE_FILENAME = "uhabits.db"
|
||||||
|
|
||||||
const val DATABASE_VERSION = 25
|
const val DATABASE_VERSION = 26
|
||||||
|
|||||||
@ -22,6 +22,7 @@ data class HabitData(
|
|||||||
var archived: Int = 0,
|
var archived: Int = 0,
|
||||||
var type: Int = 0,
|
var type: Int = 0,
|
||||||
var targetValue: Double = 0.0,
|
var targetValue: Double = 0.0,
|
||||||
|
var minutesPerUnit: Int = 0,
|
||||||
var targetType: Int = 0,
|
var targetType: Int = 0,
|
||||||
var unit: String = "",
|
var unit: String = "",
|
||||||
var uuid: String? = null
|
var uuid: String? = null
|
||||||
@ -32,7 +33,7 @@ class HabitRepository(private val db: Database) {
|
|||||||
db.prepareStatement(
|
db.prepareStatement(
|
||||||
"""SELECT id, name, description, question, freq_num, freq_den, color,
|
"""SELECT id, name, description, question, freq_num, freq_den, color,
|
||||||
position, reminder_hour, reminder_min, reminder_days, highlight,
|
position, reminder_hour, reminder_min, reminder_days, highlight,
|
||||||
archived, type, target_value, target_type, unit, uuid
|
archived, type, target_value, minutes_per_unit, target_type, unit, uuid
|
||||||
FROM Habits ORDER BY position"""
|
FROM Habits ORDER BY position"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -41,8 +42,8 @@ class HabitRepository(private val db: Database) {
|
|||||||
db.prepareStatement(
|
db.prepareStatement(
|
||||||
"""INSERT INTO Habits(name, description, question, freq_num, freq_den,
|
"""INSERT INTO Habits(name, description, question, freq_num, freq_den,
|
||||||
color, position, reminder_hour, reminder_min, reminder_days,
|
color, position, reminder_hour, reminder_min, reminder_days,
|
||||||
highlight, archived, type, target_value, target_type, unit, uuid)
|
highlight, archived, type, target_value, minutes_per_unit, target_type, unit, uuid)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,8 +51,8 @@ class HabitRepository(private val db: Database) {
|
|||||||
db.prepareStatement(
|
db.prepareStatement(
|
||||||
"""INSERT INTO Habits(id, name, description, question, freq_num, freq_den,
|
"""INSERT INTO Habits(id, name, description, question, freq_num, freq_den,
|
||||||
color, position, reminder_hour, reminder_min, reminder_days,
|
color, position, reminder_hour, reminder_min, reminder_days,
|
||||||
highlight, archived, type, target_value, target_type, unit, uuid)
|
highlight, archived, type, target_value, minutes_per_unit, target_type, unit, uuid)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ class HabitRepository(private val db: Database) {
|
|||||||
"""UPDATE Habits SET name=?, description=?, question=?, freq_num=?,
|
"""UPDATE Habits SET name=?, description=?, question=?, freq_num=?,
|
||||||
freq_den=?, color=?, position=?, reminder_hour=?, reminder_min=?,
|
freq_den=?, color=?, position=?, reminder_hour=?, reminder_min=?,
|
||||||
reminder_days=?, highlight=?, archived=?, type=?, target_value=?,
|
reminder_days=?, highlight=?, archived=?, type=?, target_value=?,
|
||||||
target_type=?, unit=?, uuid=? WHERE id=?"""
|
minutes_per_unit=?, target_type=?, unit=?, uuid=? WHERE id=?"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +95,7 @@ class HabitRepository(private val db: Database) {
|
|||||||
fun update(data: HabitData) {
|
fun update(data: HabitData) {
|
||||||
updateStmt.reset()
|
updateStmt.reset()
|
||||||
bindForInsert(updateStmt, data)
|
bindForInsert(updateStmt, data)
|
||||||
updateStmt.bindLong(18, data.id!!)
|
updateStmt.bindLong(19, data.id!!)
|
||||||
updateStmt.step()
|
updateStmt.step()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,9 +125,10 @@ class HabitRepository(private val db: Database) {
|
|||||||
stmt.bindInt(12 + o, data.archived)
|
stmt.bindInt(12 + o, data.archived)
|
||||||
stmt.bindInt(13 + o, data.type)
|
stmt.bindInt(13 + o, data.type)
|
||||||
stmt.bindReal(14 + o, data.targetValue)
|
stmt.bindReal(14 + o, data.targetValue)
|
||||||
stmt.bindInt(15 + o, data.targetType)
|
stmt.bindInt(15 + o, data.minutesPerUnit)
|
||||||
stmt.bindText(16 + o, data.unit)
|
stmt.bindInt(16 + o, data.targetType)
|
||||||
if (data.uuid != null) stmt.bindText(17 + o, data.uuid!!) else stmt.bindNull(17 + o)
|
stmt.bindText(17 + o, data.unit)
|
||||||
|
if (data.uuid != null) stmt.bindText(18 + o, data.uuid!!) else stmt.bindNull(18 + o)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readRow(stmt: PreparedStatement): HabitData {
|
private fun readRow(stmt: PreparedStatement): HabitData {
|
||||||
@ -146,9 +148,10 @@ class HabitRepository(private val db: Database) {
|
|||||||
archived = stmt.getInt(12),
|
archived = stmt.getInt(12),
|
||||||
type = stmt.getInt(13),
|
type = stmt.getInt(13),
|
||||||
targetValue = stmt.getReal(14),
|
targetValue = stmt.getReal(14),
|
||||||
targetType = stmt.getInt(15),
|
minutesPerUnit = stmt.getInt(15),
|
||||||
unit = stmt.getText(16),
|
targetType = stmt.getInt(16),
|
||||||
uuid = stmt.getTextOrNull(17)
|
unit = stmt.getText(17),
|
||||||
|
uuid = stmt.getTextOrNull(18)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -123,7 +123,7 @@ class LoopDBImporter(
|
|||||||
db.query(
|
db.query(
|
||||||
"SELECT id, name, description, question, freq_num, freq_den, color, " +
|
"SELECT id, name, description, question, freq_num, freq_den, color, " +
|
||||||
"position, reminder_hour, reminder_min, reminder_days, highlight, " +
|
"position, reminder_hour, reminder_min, reminder_days, highlight, " +
|
||||||
"archived, type, target_value, target_type, unit, uuid " +
|
"archived, type, target_value, minutes_per_unit, target_type, unit, uuid " +
|
||||||
"FROM Habits ORDER BY position"
|
"FROM Habits ORDER BY position"
|
||||||
) { stmt ->
|
) { stmt ->
|
||||||
result.add(
|
result.add(
|
||||||
@ -143,9 +143,10 @@ class LoopDBImporter(
|
|||||||
archived = stmt.getIntOrNull(12) ?: 0,
|
archived = stmt.getIntOrNull(12) ?: 0,
|
||||||
type = stmt.getIntOrNull(13) ?: 0,
|
type = stmt.getIntOrNull(13) ?: 0,
|
||||||
targetValue = stmt.getRealOrNull(14) ?: 0.0,
|
targetValue = stmt.getRealOrNull(14) ?: 0.0,
|
||||||
targetType = stmt.getIntOrNull(15) ?: 0,
|
minutesPerUnit = stmt.getIntOrNull(15) ?: 0,
|
||||||
unit = stmt.getTextOrNull(16) ?: "",
|
targetType = stmt.getIntOrNull(16) ?: 0,
|
||||||
uuid = stmt.getTextOrNull(17)
|
unit = stmt.getTextOrNull(17) ?: "",
|
||||||
|
uuid = stmt.getTextOrNull(18)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ data class Habit(
|
|||||||
var reminder: Reminder? = null,
|
var reminder: Reminder? = null,
|
||||||
var targetType: NumericalHabitType = NumericalHabitType.AT_LEAST,
|
var targetType: NumericalHabitType = NumericalHabitType.AT_LEAST,
|
||||||
var targetValue: Double = 0.0,
|
var targetValue: Double = 0.0,
|
||||||
|
var minutesPerUnit: Int = 0,
|
||||||
var type: HabitType = HabitType.YES_NO,
|
var type: HabitType = HabitType.YES_NO,
|
||||||
var unit: String = "",
|
var unit: String = "",
|
||||||
var uuid: String? = null,
|
var uuid: String? = null,
|
||||||
@ -52,15 +53,21 @@ data class Habit(
|
|||||||
val isNumerical: Boolean
|
val isNumerical: Boolean
|
||||||
get() = type == HabitType.NUMERICAL
|
get() = type == HabitType.NUMERICAL
|
||||||
|
|
||||||
|
val isTimeCost: Boolean
|
||||||
|
get() = type == HabitType.TIME_COST
|
||||||
|
|
||||||
val uriString: String
|
val uriString: String
|
||||||
get() = "content://org.isoron.uhabits/habit/$id"
|
get() = "content://org.isoron.uhabits/habit/$id"
|
||||||
|
|
||||||
fun hasReminder(): Boolean = reminder != null
|
fun hasReminder(): Boolean = reminder != null
|
||||||
|
|
||||||
|
fun getTodayValue(): Int = computedEntries.get(getToday()).value.coerceAtLeast(0)
|
||||||
|
|
||||||
fun isCompletedToday(): Boolean {
|
fun isCompletedToday(): Boolean {
|
||||||
val today = getToday()
|
val value = getTodayValue()
|
||||||
val value = computedEntries.get(today).value
|
return if (isTimeCost) {
|
||||||
return if (isNumerical) {
|
value > 0
|
||||||
|
} else if (isNumerical) {
|
||||||
when (targetType) {
|
when (targetType) {
|
||||||
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
|
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
|
||||||
NumericalHabitType.AT_MOST -> false
|
NumericalHabitType.AT_MOST -> false
|
||||||
@ -80,7 +87,7 @@ data class Habit(
|
|||||||
computedEntries.recomputeFrom(
|
computedEntries.recomputeFrom(
|
||||||
originalEntries = originalEntries,
|
originalEntries = originalEntries,
|
||||||
frequency = frequency,
|
frequency = frequency,
|
||||||
isNumerical = isNumerical
|
isNumerical = isNumerical || isTimeCost
|
||||||
)
|
)
|
||||||
|
|
||||||
val today = getToday()
|
val today = getToday()
|
||||||
@ -94,6 +101,7 @@ data class Habit(
|
|||||||
isNumerical = isNumerical,
|
isNumerical = isNumerical,
|
||||||
numericalHabitType = targetType,
|
numericalHabitType = targetType,
|
||||||
targetValue = targetValue,
|
targetValue = targetValue,
|
||||||
|
isTimeCost = isTimeCost,
|
||||||
computedEntries = computedEntries,
|
computedEntries = computedEntries,
|
||||||
from = from,
|
from = from,
|
||||||
to = to
|
to = to
|
||||||
@ -104,6 +112,7 @@ data class Habit(
|
|||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
isNumerical,
|
isNumerical,
|
||||||
|
isTimeCost,
|
||||||
targetValue,
|
targetValue,
|
||||||
targetType
|
targetType
|
||||||
)
|
)
|
||||||
@ -121,6 +130,7 @@ data class Habit(
|
|||||||
this.reminder = other.reminder
|
this.reminder = other.reminder
|
||||||
this.targetType = other.targetType
|
this.targetType = other.targetType
|
||||||
this.targetValue = other.targetValue
|
this.targetValue = other.targetValue
|
||||||
|
this.minutesPerUnit = other.minutesPerUnit
|
||||||
this.type = other.type
|
this.type = other.type
|
||||||
this.unit = other.unit
|
this.unit = other.unit
|
||||||
this.uuid = other.uuid
|
this.uuid = other.uuid
|
||||||
@ -141,6 +151,7 @@ data class Habit(
|
|||||||
if (reminder != other.reminder) return false
|
if (reminder != other.reminder) return false
|
||||||
if (targetType != other.targetType) return false
|
if (targetType != other.targetType) return false
|
||||||
if (targetValue != other.targetValue) return false
|
if (targetValue != other.targetValue) return false
|
||||||
|
if (minutesPerUnit != other.minutesPerUnit) return false
|
||||||
if (type != other.type) return false
|
if (type != other.type) return false
|
||||||
if (unit != other.unit) return false
|
if (unit != other.unit) return false
|
||||||
if (uuid != other.uuid) return false
|
if (uuid != other.uuid) return false
|
||||||
@ -160,6 +171,7 @@ data class Habit(
|
|||||||
result = 31 * result + (reminder?.hashCode() ?: 0)
|
result = 31 * result + (reminder?.hashCode() ?: 0)
|
||||||
result = 31 * result + targetType.value
|
result = 31 * result + targetType.value
|
||||||
result = 31 * result + targetValue.hashCode()
|
result = 31 * result + targetValue.hashCode()
|
||||||
|
result = 31 * result + minutesPerUnit
|
||||||
result = 31 * result + type.value
|
result = 31 * result + type.value
|
||||||
result = 31 * result + unit.hashCode()
|
result = 31 * result + unit.hashCode()
|
||||||
result = 31 * result + (uuid?.hashCode() ?: 0)
|
result = 31 * result + (uuid?.hashCode() ?: 0)
|
||||||
|
|||||||
@ -192,7 +192,7 @@ abstract class HabitList : Iterable<Habit> {
|
|||||||
numerator.toString(),
|
numerator.toString(),
|
||||||
denominator.toString(),
|
denominator.toString(),
|
||||||
habit.color.toCsvColor(),
|
habit.color.toCsvColor(),
|
||||||
if (habit.isNumerical) habit.unit else "",
|
if (habit.isNumerical || habit.isTimeCost) habit.unit else "",
|
||||||
if (habit.isNumerical) habit.targetType.name else "",
|
if (habit.isNumerical) habit.targetType.name else "",
|
||||||
if (habit.isNumerical) format("%.1f", habit.targetValue) else "",
|
if (habit.isNumerical) format("%.1f", habit.targetValue) else "",
|
||||||
habit.isArchived.toString()
|
habit.isArchived.toString()
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
package org.isoron.uhabits.core.models
|
package org.isoron.uhabits.core.models
|
||||||
|
|
||||||
enum class HabitType(val value: Int) {
|
enum class HabitType(val value: Int) {
|
||||||
YES_NO(0), NUMERICAL(1);
|
YES_NO(0),
|
||||||
|
NUMERICAL(1),
|
||||||
|
TIME_COST(2);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): HabitType {
|
fun fromInt(value: Int): HabitType {
|
||||||
return when (value) {
|
return when (value) {
|
||||||
YES_NO.value -> YES_NO
|
YES_NO.value -> YES_NO
|
||||||
NUMERICAL.value -> NUMERICAL
|
NUMERICAL.value -> NUMERICAL
|
||||||
|
TIME_COST.value -> TIME_COST
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,6 +66,7 @@ class ScoreList {
|
|||||||
fun recompute(
|
fun recompute(
|
||||||
frequency: Frequency,
|
frequency: Frequency,
|
||||||
isNumerical: Boolean,
|
isNumerical: Boolean,
|
||||||
|
isTimeCost: Boolean,
|
||||||
numericalHabitType: NumericalHabitType,
|
numericalHabitType: NumericalHabitType,
|
||||||
targetValue: Double,
|
targetValue: Double,
|
||||||
computedEntries: EntryList,
|
computedEntries: EntryList,
|
||||||
@ -119,11 +120,14 @@ class ScoreList {
|
|||||||
previousValue = compute(freq, previousValue, percentageCompleted)
|
previousValue = compute(freq, previousValue, percentageCompleted)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (values[offset] == Entry.YES_MANUAL) {
|
if (values[offset] == Entry.YES_MANUAL || (isTimeCost && values[offset] > 0)) {
|
||||||
rollingSum += 1.0
|
rollingSum += 1.0
|
||||||
}
|
}
|
||||||
if (offset + denominator < values.size) {
|
if (offset + denominator < values.size) {
|
||||||
if (values[offset + denominator] == Entry.YES_MANUAL) {
|
if (
|
||||||
|
values[offset + denominator] == Entry.YES_MANUAL ||
|
||||||
|
(isTimeCost && values[offset + denominator] > 0)
|
||||||
|
) {
|
||||||
rollingSum -= 1.0
|
rollingSum -= 1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ class StreakList {
|
|||||||
from: LocalDate,
|
from: LocalDate,
|
||||||
to: LocalDate,
|
to: LocalDate,
|
||||||
isNumerical: Boolean,
|
isNumerical: Boolean,
|
||||||
|
isTimeCost: Boolean,
|
||||||
targetValue: Double,
|
targetValue: Double,
|
||||||
targetType: NumericalHabitType
|
targetType: NumericalHabitType
|
||||||
) {
|
) {
|
||||||
@ -52,6 +53,8 @@ class StreakList {
|
|||||||
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
|
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
|
||||||
NumericalHabitType.AT_MOST -> value != Entry.UNKNOWN && value / 1000.0 <= targetValue
|
NumericalHabitType.AT_MOST -> value != Entry.UNKNOWN && value / 1000.0 <= targetValue
|
||||||
}
|
}
|
||||||
|
} else if (isTimeCost) {
|
||||||
|
value > 0
|
||||||
} else {
|
} else {
|
||||||
value > 0
|
value > 0
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,6 +140,9 @@ open class MemoryHabitList : HabitList {
|
|||||||
if (h1.isNumerical != h2.isNumerical) {
|
if (h1.isNumerical != h2.isNumerical) {
|
||||||
return@Comparator if (h1.isNumerical) -1 else 1
|
return@Comparator if (h1.isNumerical) -1 else 1
|
||||||
}
|
}
|
||||||
|
if (h1.isTimeCost != h2.isTimeCost) {
|
||||||
|
return@Comparator if (h1.isTimeCost) -1 else 1
|
||||||
|
}
|
||||||
val today = org.isoron.platform.time.getToday()
|
val today = org.isoron.platform.time.getToday()
|
||||||
val v1 = h1.computedEntries.get(today).value
|
val v1 = h1.computedEntries.get(today).value
|
||||||
val v2 = h2.computedEntries.get(today).value
|
val v2 = h2.computedEntries.get(today).value
|
||||||
|
|||||||
@ -229,6 +229,7 @@ class SQLiteHabitList(private val modelFactory: ModelFactory) : HabitList() {
|
|||||||
archived = if (habit.isArchived) 1 else 0,
|
archived = if (habit.isArchived) 1 else 0,
|
||||||
type = habit.type.value,
|
type = habit.type.value,
|
||||||
targetValue = habit.targetValue,
|
targetValue = habit.targetValue,
|
||||||
|
minutesPerUnit = habit.minutesPerUnit,
|
||||||
targetType = habit.targetType.value,
|
targetType = habit.targetType.value,
|
||||||
unit = habit.unit,
|
unit = habit.unit,
|
||||||
uuid = habit.uuid
|
uuid = habit.uuid
|
||||||
@ -246,6 +247,7 @@ class SQLiteHabitList(private val modelFactory: ModelFactory) : HabitList() {
|
|||||||
habit.type = HabitType.fromInt(data.type)
|
habit.type = HabitType.fromInt(data.type)
|
||||||
habit.targetType = NumericalHabitType.fromInt(data.targetType)
|
habit.targetType = NumericalHabitType.fromInt(data.targetType)
|
||||||
habit.targetValue = data.targetValue
|
habit.targetValue = data.targetValue
|
||||||
|
habit.minutesPerUnit = data.minutesPerUnit
|
||||||
habit.unit = data.unit
|
habit.unit = data.unit
|
||||||
habit.position = data.position
|
habit.position = data.position
|
||||||
habit.uuid = data.uuid
|
habit.uuid = data.uuid
|
||||||
|
|||||||
@ -55,7 +55,16 @@ open class ListHabitsBehavior(
|
|||||||
|
|
||||||
open fun onEdit(habit: Habit, date: LocalDate, x: Float, y: Float) {
|
open fun onEdit(habit: Habit, date: LocalDate, x: Float, y: Float) {
|
||||||
val entry = habit.computedEntries.get(date)
|
val entry = habit.computedEntries.get(date)
|
||||||
if (habit.type == HabitType.NUMERICAL) {
|
if (habit.type == HabitType.TIME_COST) {
|
||||||
|
val oldValue = entry.value.coerceAtLeast(0).toDouble()
|
||||||
|
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String ->
|
||||||
|
val value = newValue.roundToInt().coerceAtLeast(0)
|
||||||
|
if (newValue != oldValue && oldValue <= 0.0 && value > 0) {
|
||||||
|
screen.showConfetti(habit.color, x, y)
|
||||||
|
}
|
||||||
|
commandRunner.run(CreateRepetitionCommand(habitList, habit, date, value, newNotes))
|
||||||
|
}
|
||||||
|
} else if (habit.type == HabitType.NUMERICAL) {
|
||||||
val oldValue = entry.value.toDouble() / 1000
|
val oldValue = entry.value.toDouble() / 1000
|
||||||
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String ->
|
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String ->
|
||||||
val value = (newValue * 1000).roundToInt()
|
val value = (newValue * 1000).roundToInt()
|
||||||
@ -136,7 +145,11 @@ open class ListHabitsBehavior(
|
|||||||
commandRunner.run(
|
commandRunner.run(
|
||||||
CreateRepetitionCommand(habitList, habit, date, value, notes)
|
CreateRepetitionCommand(habitList, habit, date, value, notes)
|
||||||
)
|
)
|
||||||
if (value == YES_MANUAL) screen.showConfetti(habit.color, x, y)
|
if (habit.type == HabitType.TIME_COST) {
|
||||||
|
if (value > 0) screen.showConfetti(habit.color, x, y)
|
||||||
|
} else if (value == YES_MANUAL) {
|
||||||
|
screen.showConfetti(habit.color, x, y)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Message {
|
enum class Message {
|
||||||
|
|||||||
@ -173,6 +173,7 @@ class HabitRepositoryTest {
|
|||||||
archived = 1,
|
archived = 1,
|
||||||
type = 1,
|
type = 1,
|
||||||
targetValue = 10.0,
|
targetValue = 10.0,
|
||||||
|
minutesPerUnit = 11,
|
||||||
targetType = 1,
|
targetType = 1,
|
||||||
unit = "minutes",
|
unit = "minutes",
|
||||||
uuid = "550e8400-e29b-41d4-a716-446655440000"
|
uuid = "550e8400-e29b-41d4-a716-446655440000"
|
||||||
@ -194,6 +195,7 @@ class HabitRepositoryTest {
|
|||||||
assertEquals(original.archived, loaded.archived)
|
assertEquals(original.archived, loaded.archived)
|
||||||
assertEquals(original.type, loaded.type)
|
assertEquals(original.type, loaded.type)
|
||||||
assertEquals(original.targetValue, loaded.targetValue)
|
assertEquals(original.targetValue, loaded.targetValue)
|
||||||
|
assertEquals(original.minutesPerUnit, loaded.minutesPerUnit)
|
||||||
assertEquals(original.targetType, loaded.targetType)
|
assertEquals(original.targetType, loaded.targetType)
|
||||||
assertEquals(original.unit, loaded.unit)
|
assertEquals(original.unit, loaded.unit)
|
||||||
assertEquals(original.uuid, loaded.uuid)
|
assertEquals(original.uuid, loaded.uuid)
|
||||||
|
|||||||
@ -111,6 +111,16 @@ class HabitTest : BaseUnitTest() {
|
|||||||
assertFalse(h.isCompletedToday())
|
assertFalse(h.isCompletedToday())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_isCompleted_timeCost() {
|
||||||
|
val h = modelFactory.buildHabit()
|
||||||
|
h.type = HabitType.TIME_COST
|
||||||
|
assertFalse(h.isCompletedToday())
|
||||||
|
h.originalEntries.add(Entry(getToday(), 1))
|
||||||
|
h.recompute()
|
||||||
|
assertTrue(h.isCompletedToday())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testURI() {
|
fun testURI() {
|
||||||
assertTrue(habitList.isEmpty)
|
assertTrue(habitList.isEmpty)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user