Merge branch 'dev' into feature/remove-jvm-dateutils

pull/1120/head
sgallese 4 years ago
commit b870dd85f4

@ -1,6 +1,6 @@
plugins { plugins {
val kotlinVersion = "1.5.0" val kotlinVersion = "1.5.0"
id("com.android.application") version ("7.0.2") apply (false) id("com.android.application") version ("7.0.3") apply (false)
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false) id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false) id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false) id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false)
@ -18,8 +18,6 @@ allprojects {
mavenCentral() mavenCentral()
maven(url = "https://plugins.gradle.org/m2/") maven(url = "https://plugins.gradle.org/m2/")
maven(url = "https://oss.sonatype.org/content/repositories/snapshots/") maven(url = "https://oss.sonatype.org/content/repositories/snapshots/")
maven(url = "https://kotlin.bintray.com/ktor")
maven(url = "https://kotlin.bintray.com/kotlin-js-wrappers")
maven(url = "https://jitpack.io") maven(url = "https://jitpack.io")
} }
} }

@ -18,7 +18,7 @@
*/ */
plugins { plugins {
id("com.github.triplet.play") version "3.6.0" id("com.github.triplet.play") version "3.7.0"
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt") id("org.jetbrains.kotlin.kapt")
@ -86,10 +86,10 @@ android {
} }
dependencies { dependencies {
val daggerVersion = "2.39" val daggerVersion = "2.40.3"
val kotlinVersion = "1.5.31" val kotlinVersion = "1.6.0"
val kxCoroutinesVersion = "1.5.2" val kxCoroutinesVersion = "1.5.2"
val ktorVersion = "1.6.4" val ktorVersion = "1.6.6"
val espressoVersion = "3.4.0" val espressoVersion = "3.4.0"
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion") androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
@ -98,7 +98,7 @@ dependencies {
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1") androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1")
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion") androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion") androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
androidTestImplementation("androidx.annotation:annotation:1.2.0") androidTestImplementation("androidx.annotation:annotation:1.3.0")
androidTestImplementation("androidx.test.ext:junit:1.1.3") androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0") androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
androidTestImplementation("androidx.test:rules:1.4.0") androidTestImplementation("androidx.test:rules:1.4.0")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -116,15 +116,7 @@ class NumberButtonViewTest : BaseViewTest() {
} }
@Test @Test
fun testClick_shortToggleDisabled() { fun testClick() {
prefs.isShortToggleEnabled = false
view.performClick()
assertFalse(edited)
}
@Test
fun testClick_shortToggleEnabled() {
prefs.isShortToggleEnabled = true
view.performClick() view.performClick()
assertTrue(edited) assertTrue(edited)
} }

@ -61,7 +61,7 @@ class PerformanceTest : BaseAndroidTest() {
val habit = fixtures.createEmptyHabit() val habit = fixtures.createEmptyHabit()
for (i in 0..4999) { for (i in 0..4999) {
val timestamp: Timestamp = Timestamp(i * DAY_LENGTH) val timestamp: Timestamp = Timestamp(i * DAY_LENGTH)
CreateRepetitionCommand(habitList, habit, timestamp, 1).run() CreateRepetitionCommand(habitList, habit, timestamp, 1, "").run()
} }
db.setTransactionSuccessful() db.setTransactionSuccessful()
db.endTransaction() db.endTransaction()

@ -49,23 +49,12 @@ class AndroidDataView(
override fun onShowPress(e: MotionEvent?) = Unit override fun onShowPress(e: MotionEvent?) = Unit
override fun onSingleTapUp(e: MotionEvent?): Boolean { override fun onSingleTapUp(e: MotionEvent?): Boolean {
val x: Float return handleClick(e, true)
val y: Float
try {
val pointerId = e!!.getPointerId(0)
x = e.getX(pointerId)
y = e.getY(pointerId)
} catch (ex: RuntimeException) {
// Android often throws IllegalArgumentException here. Apparently,
// the pointer id may become invalid shortly after calling
// e.getPointerId.
return false
}
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
return true
} }
override fun onLongPress(e: MotionEvent?) = Unit override fun onLongPress(e: MotionEvent?) {
handleClick(e)
}
override fun onScroll( override fun onScroll(
e1: MotionEvent?, e1: MotionEvent?,
@ -137,4 +126,22 @@ class AndroidDataView(
} }
} }
} }
private fun handleClick(e: MotionEvent?, isSingleTap: Boolean = false): Boolean {
val x: Float
val y: Float
try {
val pointerId = e!!.getPointerId(0)
x = e.getX(pointerId)
y = e.getY(pointerId)
} catch (ex: RuntimeException) {
// Android often throws IllegalArgumentException here. Apparently,
// the pointer id may become invalid shortly after calling
// e.getPointerId.
return false
}
if (isSingleTap) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
else view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
return true
}
} }

@ -0,0 +1,116 @@
package org.isoron.uhabits.activities.common.dialogs
import android.content.Context
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
import android.widget.Button
import androidx.appcompat.app.AlertDialog
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry.Companion.NO
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.views.Theme
import org.isoron.uhabits.databinding.CheckmarkDialogBinding
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils
import org.isoron.uhabits.utils.StyledResources
import javax.inject.Inject
class CheckmarkDialog
@Inject constructor(
@ActivityContext private val context: Context,
private val preferences: Preferences,
) : View.OnClickListener {
private lateinit var binding: CheckmarkDialogBinding
private lateinit var fontAwesome: Typeface
private val allButtons = mutableListOf<Button>()
private var selectedButton: Button? = null
fun create(
value: Int,
notes: String,
dateString: String,
paletteColor: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback,
theme: Theme,
): AlertDialog {
binding = CheckmarkDialogBinding.inflate(LayoutInflater.from(context))
fontAwesome = InterfaceUtils.getFontAwesome(context)!!
binding.etNotes.append(notes)
setUpButtons(value, theme.color(paletteColor).toInt())
val dialog = AlertDialog.Builder(context)
.setView(binding.root)
.setTitle(dateString)
.setPositiveButton(R.string.save) { _, _ ->
val newValue = when (selectedButton?.id) {
R.id.yesBtn -> YES_MANUAL
R.id.noBtn -> NO
R.id.skippedBtn -> SKIP
else -> UNKNOWN
}
callback.onNotesSaved(newValue, binding.etNotes.text.toString())
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
callback.onNotesDismissed()
}
.setOnDismissListener {
callback.onNotesDismissed()
}
.create()
dialog.setOnShowListener {
binding.etNotes.requestFocus()
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
return dialog
}
private fun setUpButtons(value: Int, color: Int) {
val sres = StyledResources(context)
val mediumContrastColor = sres.getColor(R.attr.contrast60)
setButtonAttrs(binding.yesBtn, color)
setButtonAttrs(binding.noBtn, mediumContrastColor)
setButtonAttrs(binding.skippedBtn, color, visible = preferences.isSkipEnabled)
setButtonAttrs(binding.questionBtn, mediumContrastColor, visible = preferences.areQuestionMarksEnabled)
when (value) {
UNKNOWN -> if (preferences.areQuestionMarksEnabled) {
binding.questionBtn.performClick()
} else {
binding.noBtn.performClick()
}
SKIP -> binding.skippedBtn.performClick()
YES_MANUAL -> binding.yesBtn.performClick()
YES_AUTO, NO -> binding.noBtn.performClick()
}
}
private fun setButtonAttrs(button: Button, color: Int, visible: Boolean = true) {
button.apply {
visibility = if (visible) View.VISIBLE else View.GONE
typeface = fontAwesome
setTextColor(color)
setOnClickListener(this@CheckmarkDialog)
}
allButtons.add(button)
}
override fun onClick(v: View?) {
allButtons.forEach {
if (v?.id == it.id) {
it.isSelected = true
selectedButton = it
} else it.isSelected = false
}
}
}

@ -68,13 +68,12 @@ class FrequencyPickerDialog(
contentView.everyDayRadioButton.setOnClickListener { contentView.everyDayRadioButton.setOnClickListener {
check(contentView.everyDayRadioButton) check(contentView.everyDayRadioButton)
unfocusAll()
} }
contentView.everyXDaysRadioButton.setOnClickListener { contentView.everyXDaysRadioButton.setOnClickListener {
check(contentView.everyXDaysRadioButton) check(contentView.everyXDaysRadioButton)
val everyXDaysTextView = contentView.everyXDaysTextView val everyXDaysTextView = contentView.everyXDaysTextView
focus(everyXDaysTextView) selectInputField(everyXDaysTextView)
} }
contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus -> contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
@ -83,7 +82,7 @@ class FrequencyPickerDialog(
contentView.xTimesPerWeekRadioButton.setOnClickListener { contentView.xTimesPerWeekRadioButton.setOnClickListener {
check(contentView.xTimesPerWeekRadioButton) check(contentView.xTimesPerWeekRadioButton)
focus(contentView.xTimesPerWeekTextView) selectInputField(contentView.xTimesPerWeekTextView)
} }
contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus -> contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
@ -92,7 +91,7 @@ class FrequencyPickerDialog(
contentView.xTimesPerMonthRadioButton.setOnClickListener { contentView.xTimesPerMonthRadioButton.setOnClickListener {
check(contentView.xTimesPerMonthRadioButton) check(contentView.xTimesPerMonthRadioButton)
focus(contentView.xTimesPerMonthTextView) selectInputField(contentView.xTimesPerMonthTextView)
} }
contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus -> contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
@ -101,7 +100,7 @@ class FrequencyPickerDialog(
contentView.xTimesPerYDaysRadioButton.setOnClickListener { contentView.xTimesPerYDaysRadioButton.setOnClickListener {
check(contentView.xTimesPerYDaysRadioButton) check(contentView.xTimesPerYDaysRadioButton)
focus(contentView.xTimesPerYDaysXTextView) selectInputField(contentView.xTimesPerYDaysXTextView)
} }
contentView.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus -> contentView.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus ->
@ -185,7 +184,7 @@ class FrequencyPickerDialog(
if (freqDenominator == 30 || freqDenominator == 31) { if (freqDenominator == 30 || freqDenominator == 31) {
contentView.xTimesPerMonthRadioButton.isChecked = true contentView.xTimesPerMonthRadioButton.isChecked = true
contentView.xTimesPerMonthTextView.setText(freqNumerator.toString()) contentView.xTimesPerMonthTextView.setText(freqNumerator.toString())
focus(contentView.xTimesPerMonthTextView) selectInputField(contentView.xTimesPerMonthTextView)
} else { } else {
if (freqNumerator == 1) { if (freqNumerator == 1) {
if (freqDenominator == 1) { if (freqDenominator == 1) {
@ -193,13 +192,13 @@ class FrequencyPickerDialog(
} else { } else {
contentView.everyXDaysRadioButton.isChecked = true contentView.everyXDaysRadioButton.isChecked = true
contentView.everyXDaysTextView.setText(freqDenominator.toString()) contentView.everyXDaysTextView.setText(freqDenominator.toString())
focus(contentView.everyXDaysTextView) selectInputField(contentView.everyXDaysTextView)
} }
} else { } else {
if (freqDenominator == 7) { if (freqDenominator == 7) {
contentView.xTimesPerWeekRadioButton.isChecked = true contentView.xTimesPerWeekRadioButton.isChecked = true
contentView.xTimesPerWeekTextView.setText(freqNumerator.toString()) contentView.xTimesPerWeekTextView.setText(freqNumerator.toString())
focus(contentView.xTimesPerWeekTextView) selectInputField(contentView.xTimesPerWeekTextView)
} else { } else {
contentView.xTimesPerYDaysRadioButton.isChecked = true contentView.xTimesPerYDaysRadioButton.isChecked = true
contentView.xTimesPerYDaysXTextView.setText(freqNumerator.toString()) contentView.xTimesPerYDaysXTextView.setText(freqNumerator.toString())
@ -209,8 +208,7 @@ class FrequencyPickerDialog(
} }
} }
private fun focus(view: EditText) { private fun selectInputField(view: EditText) {
view.requestFocus()
view.setSelection(view.text.length) view.setSelection(view.text.length)
} }
@ -221,10 +219,4 @@ class FrequencyPickerDialog(
contentView.xTimesPerMonthRadioButton.isChecked = false contentView.xTimesPerMonthRadioButton.isChecked = false
contentView.xTimesPerYDaysRadioButton.isChecked = false contentView.xTimesPerYDaysRadioButton.isChecked = false
} }
private fun unfocusAll() {
contentView.everyXDaysTextView.clearFocus()
contentView.xTimesPerWeekTextView.clearFocus()
contentView.xTimesPerMonthTextView.clearFocus()
}
} }

@ -63,9 +63,10 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
paletteColor = habit.color, paletteColor = habit.color,
series = emptyList(), series = emptyList(),
defaultSquare = HistoryChart.Square.OFF, defaultSquare = HistoryChart.Square.OFF,
notesIndicators = emptyList(),
theme = themeSwitcher.currentTheme, theme = themeSwitcher.currentTheme,
today = DateUtils.getTodayWithOffset().toLocalDate(), today = DateUtils.getTodayWithOffset().toLocalDate(),
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { }, onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
padding = 10.0, padding = 10.0,
) )
dataView = AndroidDataView(context!!, null) dataView = AndroidDataView(context!!, null)
@ -103,6 +104,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
) )
chart?.series = model.series chart?.series = model.series
chart?.defaultSquare = model.defaultSquare chart?.defaultSquare = model.defaultSquare
chart?.notesIndicators = model.notesIndicators
dataView.postInvalidate() dataView.postInvalidate()
} }

@ -47,6 +47,8 @@ class NumberPickerFactory
fun create( fun create(
value: Double, value: Double,
unit: String, unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
): AlertDialog { ): AlertDialog {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
@ -54,6 +56,7 @@ class NumberPickerFactory
val picker = view.findViewById<NumberPicker>(R.id.picker) val picker = view.findViewById<NumberPicker>(R.id.picker)
val picker2 = view.findViewById<NumberPicker>(R.id.picker2) val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
val etNotes = view.findViewById<EditText>(R.id.etNotes)
val watcherFilter: InputFilter = SeparatorWatcherInputFilter(picker2) val watcherFilter: InputFilter = SeparatorWatcherInputFilter(picker2)
val numberPickerInputText = getNumberPickerInputText(picker) val numberPickerInputText = getNumberPickerInputText(picker)
@ -77,13 +80,18 @@ class NumberPickerFactory
picker2.setFormatter { v -> String.format("%02d", v) } picker2.setFormatter { v -> String.format("%02d", v) }
picker2.value = intValue % 100 picker2.value = intValue % 100
etNotes.setText(notes)
val dialog = AlertDialog.Builder(context) val dialog = AlertDialog.Builder(context)
.setView(view) .setView(view)
.setTitle(R.string.change_value) .setTitle(dateString)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(R.string.save) { _, _ ->
picker.clearFocus() picker.clearFocus()
val v = picker.value + 0.01 * picker2.value val v = picker.value + 0.01 * picker2.value
callback.onNumberPicked(v) val note = etNotes.text.toString()
callback.onNumberPicked(v, note)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
callback.onNumberPickerDismissed()
} }
.setOnDismissListener { .setOnDismissListener {
callback.onNumberPickerDismissed() callback.onNumberPickerDismissed()

@ -53,6 +53,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
override fun onQuestionMarksChanged() { override fun onQuestionMarksChanged() {
invalidateOptionsMenu() invalidateOptionsMenu()
menu.behavior.onPreferencesChanged()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

@ -39,7 +39,7 @@ class ListHabitsMenu @Inject constructor(
@ActivityContext context: Context, @ActivityContext context: Context,
private val preferences: Preferences, private val preferences: Preferences,
private val themeSwitcher: ThemeSwitcher, private val themeSwitcher: ThemeSwitcher,
private val behavior: ListHabitsMenuBehavior val behavior: ListHabitsMenuBehavior
) { ) {
val activity = (context as AppCompatActivity) val activity = (context as AppCompatActivity)

@ -25,6 +25,7 @@ import android.content.Intent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy import dagger.Lazy
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
@ -89,6 +90,7 @@ class ListHabitsScreen
private val importTaskFactory: ImportDataTaskFactory, private val importTaskFactory: ImportDataTaskFactory,
private val colorPickerFactory: ColorPickerDialogFactory, private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory, private val numberPickerFactory: NumberPickerFactory,
private val checkMarkDialog: CheckmarkDialog,
private val behavior: Lazy<ListHabitsBehavior> private val behavior: Lazy<ListHabitsBehavior>
) : CommandRunner.Listener, ) : CommandRunner.Listener,
ListHabitsBehavior.Screen, ListHabitsBehavior.Screen,
@ -225,9 +227,28 @@ class ListHabitsScreen
override fun showNumberPicker( override fun showNumberPicker(
value: Double, value: Double,
unit: String, unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
) { ) {
numberPickerFactory.create(value, unit, callback).show() numberPickerFactory.create(value, unit, notes, dateString, callback).show()
}
override fun showCheckmarkDialog(
value: Int,
notes: String,
dateString: String,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
checkMarkDialog.create(
value,
notes,
dateString,
color,
callback,
themeSwitcher.currentTheme!!,
).show()
} }
private fun getExecuteString(command: Command): String? { private fun getExecuteString(command: Command): String? {

@ -37,9 +37,9 @@ 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.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.dim import org.isoron.uhabits.utils.drawNotesIndicator
import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.sp
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toMeasureSpec import org.isoron.uhabits.utils.toMeasureSpec
import javax.inject.Inject import javax.inject.Inject
@ -71,7 +71,15 @@ class CheckmarkButtonView(
invalidate() invalidate()
} }
var hasNotes = false
set(value) {
field = value
invalidate()
}
var onToggle: (Int) -> Unit = {} var onToggle: (Int) -> Unit = {}
var onEdit: () -> Unit = {}
private var drawer = Drawer() private var drawer = Drawer()
init { init {
@ -93,11 +101,12 @@ class CheckmarkButtonView(
override fun onClick(v: View) { override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle() if (preferences.isShortToggleEnabled) performToggle()
else showMessage(resources.getString(R.string.long_press_to_toggle)) else onEdit()
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
performToggle() if (preferences.isShortToggleEnabled) onEdit()
else performToggle()
return true return true
} }
@ -145,6 +154,11 @@ class CheckmarkButtonView(
} }
else -> R.string.fa_check else -> R.string.fa_check
} }
paint.textSize = when {
id == R.string.fa_question -> sp(12.0f)
value == YES_AUTO -> sp(13.0f)
else -> sp(14.0f)
}
if (value == YES_AUTO) { if (value == YES_AUTO) {
paint.strokeWidth = 5f paint.strokeWidth = 5f
paint.style = Paint.Style.STROKE paint.style = Paint.Style.STROKE
@ -153,11 +167,6 @@ class CheckmarkButtonView(
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
} }
paint.textSize = when (id) {
UNKNOWN -> dim(R.dimen.smallerTextSize)
else -> dim(R.dimen.smallTextSize)
}
val label = resources.getString(id) val label = resources.getString(id)
val em = paint.measureText("m") val em = paint.measureText("m")
@ -170,6 +179,8 @@ class CheckmarkButtonView(
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
canvas.drawText(label, rect.centerX(), rect.centerY(), paint) canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
} }
drawNotesIndicator(canvas, color, em, hasNotes)
} }
} }
} }

@ -54,12 +54,24 @@ class CheckmarkPanelView(
setupButtons() setupButtons()
} }
var notesIndicators = BooleanArray(0)
set(values) {
field = values
setupButtons()
}
var onToggle: (Timestamp, Int) -> Unit = { _, _ -> } var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
} }
var onEdit: (Timestamp) -> Unit = {}
set(value) {
field = value
setupButtons()
}
override fun createButton(): CheckmarkButtonView = buttonFactory.create() override fun createButton(): CheckmarkButtonView = buttonFactory.create()
@Synchronized @Synchronized
@ -72,8 +84,13 @@ class CheckmarkPanelView(
index + dataOffset < values.size -> values[index + dataOffset] index + dataOffset < values.size -> values[index + dataOffset]
else -> UNKNOWN else -> UNKNOWN
} }
button.hasNotes = when {
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset]
else -> false
}
button.color = color button.color = color
button.onToggle = { value -> onToggle(timestamp, value) } button.onToggle = { value -> onToggle(timestamp, value) }
button.onEdit = { onEdit(timestamp) }
} }
} }
} }

@ -124,8 +124,9 @@ class HabitCardListAdapter @Inject constructor(
val habit = cache.getHabitByPosition(position) val habit = cache.getHabitByPosition(position)
val score = cache.getScore(habit!!.id!!) val score = cache.getScore(habit!!.id!!)
val checkmarks = cache.getCheckmarks(habit.id!!) val checkmarks = cache.getCheckmarks(habit.id!!)
val notesIndicators = cache.getNoteIndicators(habit.id!!)
val selected = selected.contains(habit) val selected = selected.contains(habit)
listView!!.bindCardView(holder, habit, score, checkmarks, selected) listView!!.bindCardView(holder, habit, score, checkmarks, notesIndicators, selected)
} }
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) { override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {

@ -87,6 +87,7 @@ class HabitCardListView(
habit: Habit, habit: Habit,
score: Double, score: Double,
checkmarks: IntArray, checkmarks: IntArray,
notesIndicators: BooleanArray,
selected: Boolean selected: Boolean
): View { ): View {
val cardView = holder.itemView as HabitCardView val cardView = holder.itemView as HabitCardView
@ -98,6 +99,7 @@ class HabitCardListView(
cardView.score = score cardView.score = score
cardView.unit = habit.unit cardView.unit = habit.unit
cardView.threshold = habit.targetValue / habit.frequency.denominator cardView.threshold = habit.targetValue / habit.frequency.denominator
cardView.notesIndicators = notesIndicators
val detector = GestureDetector(context, CardViewGestureDetector(holder)) val detector = GestureDetector(context, CardViewGestureDetector(holder))
cardView.setOnTouchListener { _, ev -> cardView.setOnTouchListener { _, ev ->

@ -36,7 +36,6 @@ import android.widget.TextView
import org.isoron.platform.gui.toInt import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.RingView import org.isoron.uhabits.activities.common.views.RingView
import org.isoron.uhabits.activities.habits.list.views.HabitCardView.Companion.delay
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.ModelObservable import org.isoron.uhabits.core.models.ModelObservable
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
@ -58,6 +57,12 @@ class HabitCardViewFactory
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior) fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
} }
data class DelayedToggle(
var habit: Habit,
var timestamp: Timestamp,
var value: Int
)
class HabitCardView( class HabitCardView(
@ActivityContext context: Context, @ActivityContext context: Context,
checkmarkPanelFactory: CheckmarkPanelViewFactory, checkmarkPanelFactory: CheckmarkPanelViewFactory,
@ -116,12 +121,22 @@ class HabitCardView(
numberPanel.threshold = value numberPanel.threshold = value
} }
var notesIndicators
get() = checkmarkPanel.notesIndicators
set(values) {
checkmarkPanel.notesIndicators = values
numberPanel.notesIndicators = values
}
var checkmarkPanel: CheckmarkPanelView var checkmarkPanel: CheckmarkPanelView
private var numberPanel: NumberPanelView private var numberPanel: NumberPanelView
private var innerFrame: LinearLayout private var innerFrame: LinearLayout
private var label: TextView private var label: TextView
private var scoreRing: RingView private var scoreRing: RingView
private var currentToggleTaskId = 0
private var queuedToggles = mutableListOf<DelayedToggle>()
init { init {
scoreRing = RingView(context).apply { scoreRing = RingView(context).apply {
val thickness = dp(3f) val thickness = dp(3f)
@ -145,11 +160,14 @@ class HabitCardView(
onToggle = { timestamp, value -> onToggle = { timestamp, value ->
triggerRipple(timestamp) triggerRipple(timestamp)
habit?.let { habit?.let {
{ val taskId = queueToggle(it, timestamp, value);
behavior.onToggle(it, timestamp, value) { runPendingToggles(taskId) }.delay(TOGGLE_DELAY_MILLIS)
}.delay(TOGGLE_DELAY_MILLIS)
} }
} }
onEdit = { timestamp ->
triggerRipple(timestamp)
habit?.let { behavior.onEdit(it, timestamp) }
}
} }
numberPanel = numberPanelFactory.create().apply { numberPanel = numberPanelFactory.create().apply {
@ -184,6 +202,24 @@ class HabitCardView(
addView(innerFrame) addView(innerFrame)
} }
@Synchronized
private fun runPendingToggles(id: Int) {
if (currentToggleTaskId != id) return
for ((h, t, v) in queuedToggles) behavior.onToggle(h, t, v)
queuedToggles.clear()
}
@Synchronized
private fun queueToggle(
it: Habit,
timestamp: Timestamp,
value: Int
): Int {
currentToggleTaskId += 1
queuedToggles.add(DelayedToggle(it, timestamp, value))
return currentToggleTaskId
}
override fun onModelChange() { override fun onModelChange() {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
habit?.let { copyAttributesFrom(it) } habit?.let { copyAttributesFrom(it) }

@ -34,8 +34,8 @@ import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils.getDimension import org.isoron.uhabits.utils.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.dim import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.drawNotesIndicator
import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
import java.lang.Double.max import java.lang.Double.max
import java.text.DecimalFormat import java.text.DecimalFormat
@ -101,6 +101,11 @@ class NumberButtonView(
field = value field = value
invalidate() invalidate()
} }
var hasNotes = false
set(value) {
field = value
invalidate()
}
var onEdit: () -> Unit = {} var onEdit: () -> Unit = {}
private var drawer: Drawer = Drawer(context) private var drawer: Drawer = Drawer(context)
@ -111,8 +116,7 @@ class NumberButtonView(
} }
override fun onClick(v: View) { override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) onEdit() onEdit()
else showMessage(resources.getString(R.string.long_press_to_edit))
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
@ -211,6 +215,8 @@ class NumberButtonView(
rect.offset(0f, 1.3f * em) rect.offset(0f, 1.3f * em)
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit) canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
} }
drawNotesIndicator(canvas, color, em, hasNotes)
} }
} }
} }

@ -72,6 +72,12 @@ class NumberPanelView(
setupButtons() setupButtons()
} }
var notesIndicators = BooleanArray(0)
set(values) {
field = values
setupButtons()
}
var onEdit: (Timestamp) -> Unit = {} var onEdit: (Timestamp) -> Unit = {}
set(value) { set(value) {
field = value field = value
@ -90,6 +96,10 @@ class NumberPanelView(
index + dataOffset < values.size -> values[index + dataOffset] index + dataOffset < values.size -> values[index + dataOffset]
else -> 0.0 else -> 0.0
} }
button.hasNotes = when {
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset]
else -> false
}
button.color = color button.color = color
button.targetType = targetType button.targetType = targetType
button.threshold = threshold button.threshold = threshold

@ -32,12 +32,14 @@ import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.HabitsDirFinder import org.isoron.uhabits.activities.HabitsDirFinder
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.commands.Command import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
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.callbacks.OnConfirmedCallback import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
@ -164,9 +166,29 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
override fun showNumberPicker( override fun showNumberPicker(
value: Double, value: Double,
unit: String, unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback, callback: ListHabitsBehavior.NumberPickerCallback,
) { ) {
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, callback).show() NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, callback).show()
}
override fun showCheckmarkDialog(
value: Int,
notes: String,
dateString: String,
preferences: Preferences,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
CheckmarkDialog(this@ShowHabitActivity, preferences).create(
value,
notes,
dateString,
color,
callback,
themeSwitcher.currentTheme!!,
).show()
} }
override fun showEditHabitScreen(habit: Habit) { override fun showEditHabitScreen(habit: Habit) {

@ -44,6 +44,7 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()), dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
series = state.series, series = state.series,
defaultSquare = state.defaultSquare, defaultSquare = state.defaultSquare,
notesIndicators = state.notesIndicators,
firstWeekday = state.firstWeekday, firstWeekday = state.firstWeekday,
) )
binding.chart.postInvalidate() binding.chart.postInvalidate()

@ -23,17 +23,17 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.HabitMatcherBuilder import org.isoron.uhabits.core.models.HabitMatcher
class EditSettingActivity : AppCompatActivity() { class EditSettingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val app = applicationContext as HabitsApplication val app = applicationContext as HabitsApplication
val habits = app.component.habitList.getFiltered( val habits = app.component.habitList.getFiltered(
HabitMatcherBuilder() HabitMatcher(
.setArchivedAllowed(false) isArchivedAllowed = false,
.setCompletedAllowed(true) isCompletedAllowed = true,
.build() )
) )
AndroidThemeSwitcher(this, app.component.preferences).apply() AndroidThemeSwitcher(this, app.component.preferences).apply()

@ -22,7 +22,9 @@ package org.isoron.uhabits.utils
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Handler import android.os.Handler
import android.view.LayoutInflater import android.view.LayoutInflater
@ -199,5 +201,15 @@ fun View.dim(id: Int) = InterfaceUtils.getDimension(context, id)
fun View.sp(value: Float) = InterfaceUtils.spToPixels(context, value) fun View.sp(value: Float) = InterfaceUtils.spToPixels(context, value)
fun View.dp(value: Float) = InterfaceUtils.dpToPixels(context, value) fun View.dp(value: Float) = InterfaceUtils.dpToPixels(context, value)
fun View.str(id: Int) = resources.getString(id) fun View.str(id: Int) = resources.getString(id)
fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, hasNotes: Boolean) {
val pNotesIndicator = Paint()
pNotesIndicator.color = color
if (hasNotes) {
val cy = 0.8f * size
canvas.drawCircle(width.toFloat() - cy, cy, 8f, pNotesIndicator)
}
}
val View.sres: StyledResources val View.sres: StyledResources
get() = StyledResources(context) get() = StyledResources(context)

@ -59,6 +59,7 @@ class HistoryWidget(
val historyChart = (this.view as HistoryChart) val historyChart = (this.view as HistoryChart)
historyChart.series = model.series historyChart.series = model.series
historyChart.defaultSquare = model.defaultSquare historyChart.defaultSquare = model.defaultSquare
historyChart.notesIndicators = model.notesIndicators
} }
} }
@ -74,6 +75,7 @@ class HistoryWidget(
firstWeekday = prefs.firstWeekday, firstWeekday = prefs.firstWeekday,
series = listOf(), series = listOf(),
defaultSquare = HistoryChart.Square.OFF, defaultSquare = HistoryChart.Square.OFF,
notesIndicators = listOf(),
) )
} }
).apply { ).apply {

@ -60,8 +60,8 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
SystemUtils.unlockScreen(this) SystemUtils.unlockScreen(this)
} }
override fun onNumberPicked(newValue: Double) { override fun onNumberPicked(newValue: Double, notes: String) {
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt()) behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt(), notes)
widgetUpdater.updateWidgets() widgetUpdater.updateWidgets()
finish() finish()
} }
@ -79,6 +79,8 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
numberPickerFactory.create( numberPickerFactory.create(
entry.value / 1000.0, entry.value / 1000.0,
data.habit.unit, data.habit.unit,
entry.notes,
today.toDialogDateString(),
this this
).show() ).show()
} }

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape>
<solid android:color="?attr/contrast40" />
<corners android:radius="4dp"/>
<padding
android:bottom="0dp"
android:left="8dp"
android:right="8dp"
android:top="0dp" />
</shape>
</item>
</selector>

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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/>.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="1dp" android:color="?attr/contrast40" />
<corners android:radius="4dp"/>
<padding
android:left="0dp"
android:top="8dp"
android:right="0dp"
android:bottom="0dp" />
</shape>

@ -120,17 +120,20 @@
linked at the bottom of the list. If you qualify, please feel free to linked at the bottom of the list. If you qualify, please feel free to
submit a pull request adding yourself here. submit a pull request adding yourself here.
--> -->
<TextView style="@style/About.Item" android:text="Álinson Santos Xavier"/> <TextView style="@style/About.Item" android:text="Álinson S. Xavier (@iSoron)"/>
<TextView style="@style/About.Item" android:text="Quentin Hibon"/> <TextView style="@style/About.Item" android:text="Quentin Hibon (@hiqua)"/>
<TextView style="@style/About.Item" android:text="Oleg Ivashchenko"/> <TextView style="@style/About.Item" android:text="Oleg Ivashchenko (@olegivo)"/>
<TextView style="@style/About.Item" android:text="Rechee Jozil"/> <TextView style="@style/About.Item" android:text="Rechee Jozil (@recheej)"/>
<TextView style="@style/About.Item" android:text="Luboš Luňák"/> <TextView style="@style/About.Item" android:text="Jakub Kalinowski (@kalina559)"/>
<TextView style="@style/About.Item" android:text="Kristian Tashkov"/> <TextView style="@style/About.Item" android:text="Luboš Luňák (@llunak)"/>
<TextView style="@style/About.Item" android:text="Victor Yu"/> <TextView style="@style/About.Item" android:text="Sebastian Gallese (@sgallese)"/>
<TextView style="@style/About.Item" android:text="Денис (sciamano)"/> <TextView style="@style/About.Item" android:text="Kristian Tashkov (@KristianTashkov)"/>
<TextView style="@style/About.Item" android:text="Christoph Hennemann"/> <TextView style="@style/About.Item" android:text="Bindu (@vbh)"/>
<TextView style="@style/About.Item" android:text="Joseph Tran"/> <TextView style="@style/About.Item" android:text="Victor Yu (@vyu1)"/>
<TextView style="@style/About.Item" android:text="Nikhil (regularcoder)"/> <TextView style="@style/About.Item" android:text="Christoph Hennemann (@chennemann)"/>
<TextView style="@style/About.Item" android:text="Денис (@sciamano)"/>
<TextView style="@style/About.Item" android:text="Joseph Tran (@JotraN)"/>
<TextView style="@style/About.Item" android:text="Nikhil (@regularcoder)"/>
<TextView style="@style/About.Item" android:text="JanetQC"/> <TextView style="@style/About.Item" android:text="JanetQC"/>
<TextView <TextView
android:id="@+id/tvContributors" android:id="@+id/tvContributors"

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="12dp"
android:paddingStart="10dp"
android:paddingEnd="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:baselineAligned="false">
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_weight="1">
<LinearLayout style="@style/DialogFormInnerBox">
<TextView
style="@style/DialogFormLabel"
android:text="@string/value" />
<LinearLayout
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="8dp">
<Button
android:id="@+id/yesBtn"
android:text="@string/fa_check"
style="@style/CheckmarkDialogBtn"/>
<Button
android:id="@+id/skippedBtn"
android:text="@string/fa_skipped"
android:visibility="gone"
style="@style/CheckmarkDialogBtn"/>
<Button
android:id="@+id/noBtn"
android:text="@string/fa_times"
style="@style/CheckmarkDialogBtn"/>
<Button
android:id="@+id/questionBtn"
android:text="@string/fa_question"
android:visibility="gone"
style="@style/CheckmarkDialogBtn"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp"
android:baselineAligned="false">
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_weight="1">
<LinearLayout style="@style/DialogFormInnerBox">
<TextView
style="@style/DialogFormLabel"
android:text="@string/notes" />
<EditText
android:id="@+id/etNotes"
android:inputType="textCapSentences|textMultiLine"
style="@style/FormInput"
android:scrollbars="vertical"
android:hint="@string/example_notes"/>
</LinearLayout>
</FrameLayout>
</LinearLayout>
</LinearLayout>

@ -19,33 +19,98 @@
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:orientation="vertical"
android:gravity="center" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content"> android:paddingTop="12dp"
android:paddingStart="10dp"
<NumberPicker android:paddingEnd="10dp">
android:id="@+id/picker"
android:layout_gravity="center" <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tvSeparator"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
/> android:orientation="horizontal"
android:padding="5dp"
android:baselineAligned="false">
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_weight="1">
<LinearLayout style="@style/DialogFormInnerBox">
<TextView
style="@style/DialogFormLabel"
android:text="@string/value" />
<LinearLayout
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<NumberPicker
android:id="@+id/picker"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tvSeparator"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<NumberPicker
android:id="@+id/picker2"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/tvUnit"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>
<NumberPicker </LinearLayout>
android:id="@+id/picker2"
android:layout_gravity="center"
android:layout_width="wrap_content" <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
/> android:orientation="horizontal"
android:padding="5dp"
android:baselineAligned="false">
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_weight="1">
<LinearLayout style="@style/DialogFormInnerBox">
<TextView
style="@style/DialogFormLabel"
android:text="@string/notes" />
<EditText
android:id="@+id/etNotes"
android:inputType="textCapSentences|textMultiLine"
style="@style/FormInput"
android:scrollbars="vertical"
android:hint="@string/example_notes"/>
</LinearLayout>
</FrameLayout>
<TextView </LinearLayout>
android:id="@+id/tvUnit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout> </LinearLayout>

@ -43,6 +43,7 @@
<attr name="iconFilter" format="reference"/> <attr name="iconFilter" format="reference"/>
<attr name="iconArrowUp" format="reference"/> <attr name="iconArrowUp" format="reference"/>
<attr name="iconArrowDown" format="reference"/> <attr name="iconArrowDown" format="reference"/>
<attr name="dialogFormLabelColor" format="reference"/>
<attr name="toolbarPopupTheme" format="reference"/> <attr name="toolbarPopupTheme" format="reference"/>

@ -80,7 +80,7 @@
<string name="interval_always_ask">Always ask</string> <string name="interval_always_ask">Always ask</string>
<string name="interval_custom">Custom...</string> <string name="interval_custom">Custom...</string>
<string name="pref_toggle_title">Toggle with short press</string> <string name="pref_toggle_title">Toggle with short press</string>
<string name="pref_toggle_description">Put checkmarks with a single tap instead of press-and-hold. More convenient, but might cause accidental toggles.</string> <string name="pref_toggle_description_2">Put checkmarks with a single tap instead of press-and-hold.</string>
<string name="pref_rate_this_app">Rate this app on Google Play</string> <string name="pref_rate_this_app">Rate this app on Google Play</string>
<string name="pref_send_feedback">Send feedback to developer</string> <string name="pref_send_feedback">Send feedback to developer</string>
<string name="pref_view_source_code">View source code at GitHub</string> <string name="pref_view_source_code">View source code at GitHub</string>
@ -181,7 +181,7 @@
<string name="by_status">By status</string> <string name="by_status">By status</string>
<string name="export">Export</string> <string name="export">Export</string>
<string name="long_press_to_edit">Press-and-hold to change the value</string> <string name="long_press_to_edit">Press-and-hold to change the value</string>
<string name="change_value">Change value</string> <string name="value">Value</string>
<string name="calendar">Calendar</string> <string name="calendar">Calendar</string>
<string name="unit">Unit</string> <string name="unit">Unit</string>
<string name="target_type">Target Type</string> <string name="target_type">Target Type</string>

@ -63,6 +63,7 @@
<item name="windowBackgroundColor">@color/grey_200</item> <item name="windowBackgroundColor">@color/grey_200</item>
<item name="android:textColorAlertDialogListItem">@color/grey_800</item> <item name="android:textColorAlertDialogListItem">@color/grey_800</item>
<item name="singleLineTitle">false</item> <item name="singleLineTitle">false</item>
<item name="dialogFormLabelColor">@color/white</item>
</style> </style>
<style name="AppBaseThemeDark" parent="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"> <style name="AppBaseThemeDark" parent="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
@ -110,6 +111,7 @@
<item name="buttonBarPositiveButtonStyle">@style/DialogButtonStyle</item> <item name="buttonBarPositiveButtonStyle">@style/DialogButtonStyle</item>
<item name="android:textColorAlertDialogListItem">@color/grey_100</item> <item name="android:textColorAlertDialogListItem">@color/grey_100</item>
<item name="singleLineTitle">false</item> <item name="singleLineTitle">false</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style> </style>
<style name="AppBaseThemeDark.PureBlack"> <style name="AppBaseThemeDark.PureBlack">
@ -130,6 +132,7 @@
<item name="textColorAlertDialogListItem">@color/grey_100</item> <item name="textColorAlertDialogListItem">@color/grey_100</item>
<item name="windowBackgroundColor">@color/black</item> <item name="windowBackgroundColor">@color/black</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material.PureBlack</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material.PureBlack</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style> </style>
<style name="BaseDialog" parent="Theme.AppCompat.Light.Dialog"> <style name="BaseDialog" parent="Theme.AppCompat.Light.Dialog">
@ -140,6 +143,7 @@
<item name="contrast80">@color/grey_700</item> <item name="contrast80">@color/grey_700</item>
<item name="contrast100">@color/grey_800</item> <item name="contrast100">@color/grey_800</item>
<item name="palette">@array/lightPalette</item> <item name="palette">@array/lightPalette</item>
<item name="dialogFormLabelColor">@color/white</item>
</style> </style>
<style name="BaseDialogDark" parent="Theme.AppCompat.Dialog"> <style name="BaseDialogDark" parent="Theme.AppCompat.Dialog">
@ -150,6 +154,7 @@
<item name="contrast80">@color/grey_300</item> <item name="contrast80">@color/grey_300</item>
<item name="contrast100">@color/grey_100</item> <item name="contrast100">@color/grey_100</item>
<item name="palette">@array/darkPalette</item> <item name="palette">@array/darkPalette</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style> </style>
<style name="PreferenceThemeOverlay.v14.Material.PureBlack"> <style name="PreferenceThemeOverlay.v14.Material.PureBlack">
@ -360,4 +365,38 @@
<item name="android:layout_height">1dp</item> <item name="android:layout_height">1dp</item>
<item name="android:background">?attr/contrast20</item> <item name="android:background">?attr/contrast20</item>
</style> </style>
<style name="DialogFormInnerBox">
<item name="android:background">@drawable/dialog_bg_input_box</item>
<item name="android:clipChildren">false</item>
<item name="android:clipToPadding">false</item>
<item name="android:orientation">vertical</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">match_parent</item>
</style>
<style name="DialogFormLabel">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginStart">8dp</item>
<item name="android:layout_marginTop">-15dp</item>
<item name="android:layout_marginBottom">-4dp</item>
<item name="android:paddingStart">8dp</item>
<item name="android:background">?attr/dialogFormLabelColor</item>
<item name="android:paddingEnd">8dp</item>
<item name="android:textSize">@dimen/smallTextSize</item>
</style>
<style name="CheckmarkDialogBtn">
<item name="android:layout_width">48dp</item>
<item name="android:layout_height">48dp</item>
<item name="android:layout_marginTop">8dp</item>
<item name="android:layout_marginBottom">8dp</item>
<item name="android:layout_marginEnd">12dp</item>
<item name="android:textSize">@dimen/regularTextSize</item>
<item name="backgroundTint">@null</item>
<item name="android:background">@drawable/bg_select_button</item>
<item name="selectable">true</item>
</style>
</resources> </resources>

@ -27,7 +27,7 @@
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="pref_short_toggle" android:key="pref_short_toggle"
android:summary="@string/pref_toggle_description" android:summary="@string/pref_toggle_description_2"
android:title="@string/pref_toggle_title" android:title="@string/pref_toggle_title"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />

@ -0,0 +1 @@
2015-01-25,0.0000
1 2015-01-25 0.0000

@ -0,0 +1,10 @@
2015-01-25,2
2015-01-24,0
2015-01-23,1
2015-01-22,2
2015-01-21,2
2015-01-20,2
2015-01-19,1
2015-01-18,1
2015-01-17,2
2015-01-16,2
1 2015-01-25 2
2 2015-01-24 0
3 2015-01-23 1
4 2015-01-22 2
5 2015-01-21 2
6 2015-01-20 2
7 2015-01-19 1
8 2015-01-18 1
9 2015-01-17 2
10 2015-01-16 2

@ -0,0 +1,10 @@
2015-01-25,0.2557
2015-01-24,0.2226
2015-01-23,0.1991
2015-01-22,0.1746
2015-01-21,0.1379
2015-01-20,0.0995
2015-01-19,0.0706
2015-01-18,0.0515
2015-01-17,0.0315
2015-01-16,0.0107
1 2015-01-25 0.2557
2 2015-01-24 0.2226
3 2015-01-23 0.1991
4 2015-01-22 0.1746
5 2015-01-21 0.1379
6 2015-01-20 0.0995
7 2015-01-19 0.0706
8 2015-01-18 0.0515
9 2015-01-17 0.0315
10 2015-01-16 0.0107

@ -0,0 +1,11 @@
Date,Meditate,Wake up early,
2015-01-25,-1,2,
2015-01-24,-1,0,
2015-01-23,-1,1,
2015-01-22,-1,2,
2015-01-21,-1,2,
2015-01-20,-1,2,
2015-01-19,-1,1,
2015-01-18,-1,1,
2015-01-17,-1,2,
2015-01-16,-1,2,
1 Date Meditate Wake up early
2 2015-01-25 -1 2
3 2015-01-24 -1 0
4 2015-01-23 -1 1
5 2015-01-22 -1 2
6 2015-01-21 -1 2
7 2015-01-20 -1 2
8 2015-01-19 -1 1
9 2015-01-18 -1 1
10 2015-01-17 -1 2
11 2015-01-16 -1 2

@ -0,0 +1,3 @@
Position,Name,Question,Description,NumRepetitions,Interval,Color
001,Meditate,Did you meditate this morning?,,1,1,#FF8F00
002,Wake up early,Did you wake up before 6am?,,2,3,#00897B
1 Position Name Question Description NumRepetitions Interval Color
2 001 Meditate Did you meditate this morning? 1 1 #FF8F00
3 002 Wake up early Did you wake up before 6am? 2 3 #00897B

@ -0,0 +1,11 @@
Date,Meditate,Wake up early,
2015-01-25,0.0000,0.2557,
2015-01-24,0.0000,0.2226,
2015-01-23,0.0000,0.1991,
2015-01-22,0.0000,0.1746,
2015-01-21,0.0000,0.1379,
2015-01-20,0.0000,0.0995,
2015-01-19,0.0000,0.0706,
2015-01-18,0.0000,0.0515,
2015-01-17,0.0000,0.0315,
2015-01-16,0.0000,0.0107,
1 Date Meditate Wake up early
2 2015-01-25 0.0000 0.2557
3 2015-01-24 0.0000 0.2226
4 2015-01-23 0.0000 0.1991
5 2015-01-22 0.0000 0.1746
6 2015-01-21 0.0000 0.1379
7 2015-01-20 0.0000 0.0995
8 2015-01-19 0.0000 0.0706
9 2015-01-18 0.0000 0.0515
10 2015-01-17 0.0000 0.0315
11 2015-01-16 0.0000 0.0107

@ -1,5 +1,5 @@
HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText
Breed dragons,with love and fire,Diet & Food,2016-03-18,1, Breed dragons,with love and fire,Diet & Food,2016-03-18,1,text
Breed dragons,with love and fire,Diet & Food,2016-03-19,1, Breed dragons,with love and fire,Diet & Food,2016-03-19,1,
Breed dragons,with love and fire,Diet & Food,2016-03-21,1, Breed dragons,with love and fire,Diet & Food,2016-03-21,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-15,1, Reduce sleep,only 2 hours per day,Time Management,2016-03-15,1,

1 HabitName HabitDescription HabitCategory CalendarDate Value CommentText
2 Breed dragons with love and fire Diet & Food 2016-03-18 1 text
3 Breed dragons with love and fire Diet & Food 2016-03-19 1
4 Breed dragons with love and fire Diet & Food 2016-03-21 1
5 Reduce sleep only 2 hours per day Time Management 2016-03-15 1

@ -1,7 +1,7 @@
HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText
H1,,C1,11/5/2020,1, H1,,C1,11/5/2020,1,
H2,,C2,11/5/2020,-2150000000, H2,,C2,11/5/2020,-2150000000,
H3,Habit 3,C3,4/11/2019,1, H3,Habit 3,C3,4/11/2019,1,text
H3,Habit 3,C3,4/12/2019,1, H3,Habit 3,C3,4/12/2019,1,
H3,Habit 3,C3,4/13/2019,0, H3,Habit 3,C3,4/13/2019,0,
H3,Habit 3,C3,4/14/2019,1, H3,Habit 3,C3,4/14/2019,1,
@ -65,7 +65,7 @@ H3,Habit 3,C3,6/10/2019,1,
H3,Habit 3,C3,6/11/2019,1, H3,Habit 3,C3,6/11/2019,1,
H3,Habit 3,C3,6/12/2019,1, H3,Habit 3,C3,6/12/2019,1,
H3,Habit 3,C3,6/13/2019,1, H3,Habit 3,C3,6/13/2019,1,
H3,Habit 3,C3,6/14/2019,0, H3,Habit 3,C3,6/14/2019,0,Habit 3 notes
H3,Habit 3,C3,6/15/2019,1, H3,Habit 3,C3,6/15/2019,1,
H4,Habit 4,C4,11/6/2020,1, H4,Habit 4,C4,11/6/2020,1,
H4,Habit 4,C4,11/9/2020,1, H4,Habit 4,C4,11/9/2020,1,

1 HabitName HabitDescription HabitCategory CalendarDate Value CommentText
2 H1 C1 11/5/2020 1
3 H2 C2 11/5/2020 -2150000000
4 H3 Habit 3 C3 4/11/2019 1 text
5 H3 Habit 3 C3 4/12/2019 1
6 H3 Habit 3 C3 4/13/2019 0
7 H3 Habit 3 C3 4/14/2019 1
65 H3 Habit 3 C3 6/11/2019 1
66 H3 Habit 3 C3 6/12/2019 1
67 H3 Habit 3 C3 6/13/2019 1
68 H3 Habit 3 C3 6/14/2019 0 Habit 3 notes
69 H3 Habit 3 C3 6/15/2019 1
70 H4 Habit 4 C4 11/6/2020 1
71 H4 Habit 4 C4 11/9/2020 1

@ -45,11 +45,11 @@ kotlin {
val jvmMain by getting { val jvmMain by getting {
dependencies { dependencies {
implementation(kotlin("stdlib-jdk8")) implementation(kotlin("stdlib-jdk8"))
compileOnly("com.google.dagger:dagger:2.39") compileOnly("com.google.dagger:dagger:2.40.3")
implementation("com.google.guava:guava:31.0.1-android") implementation("com.google.guava:guava:31.0.1-android")
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.31") implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2")
implementation("androidx.annotation:annotation:1.2.0") implementation("androidx.annotation:annotation:1.3.0")
implementation("com.google.code.findbugs:jsr305:3.0.2") implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.opencsv:opencsv:5.5.2") implementation("com.opencsv:opencsv:5.5.2")
implementation("commons-codec:commons-codec:1.15") implementation("commons-codec:commons-codec:1.15")

@ -23,6 +23,8 @@ interface View {
fun draw(canvas: Canvas) fun draw(canvas: Canvas)
fun onClick(x: Double, y: Double) { fun onClick(x: Double, y: Double) {
} }
fun onLongClick(x: Double, y: Double) {
}
} }
interface DataView : View { interface DataView : View {

@ -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 = 24 const val DATABASE_VERSION = 25

@ -28,10 +28,11 @@ data class CreateRepetitionCommand(
val habit: Habit, val habit: Habit,
val timestamp: Timestamp, val timestamp: Timestamp,
val value: Int, val value: Int,
val notes: String,
) : Command { ) : Command {
override fun run() { override fun run() {
val entries = habit.originalEntries val entries = habit.originalEntries
entries.add(Entry(timestamp, value)) entries.add(Entry(timestamp, value, notes))
habit.recompute() habit.recompute()
habitList.resort() habitList.resort()
} }

@ -76,8 +76,11 @@ class HabitBullCSVImporter
map[name] = h map[name] = h
logger.info("Creating habit: $name") logger.info("Creating habit: $name")
} }
val notes = cols[5] ?: ""
if (parseInt(cols[4]) == 1) { if (parseInt(cols[4]) == 1) {
h.originalEntries.add(Entry(timestamp, Entry.YES_MANUAL)) h.originalEntries.add(Entry(timestamp, Entry.YES_MANUAL, notes))
} else {
h.originalEntries.add(Entry(timestamp, Entry.NO, notes))
} }
} }
} }

@ -168,7 +168,7 @@ class HabitsCSVExporter(
checksWriter.write(sb.toString()) checksWriter.write(sb.toString())
scoresWriter.write(sb.toString()) scoresWriter.write(sb.toString())
for (j in selectedHabits.indices) { for (j in selectedHabits.indices) {
checksWriter.write(checkmarks[j][i].toString()) checksWriter.write(checkmarks[j][i].value.toString())
checksWriter.write(delimiter) checksWriter.write(delimiter)
val score = String.format(Locale.US, "%.4f", scores[j][i].value) val score = String.format(Locale.US, "%.4f", scores[j][i].value)
scoresWriter.write(score) scoresWriter.write(score)

@ -101,8 +101,9 @@ class LoopDBImporter
for (r in entryRecords) { for (r in entryRecords) {
val t = Timestamp(r.timestamp!!) val t = Timestamp(r.timestamp!!)
val (_, value) = habit!!.originalEntries.get(t) val (_, value, notes) = habit!!.originalEntries.get(t)
if (value != r.value) CreateRepetitionCommand(habitList, habit, t, r.value!!).run() val oldNotes = r.notes ?: ""
if (value != r.value || notes != oldNotes) CreateRepetitionCommand(habitList, habit, t, r.value!!, oldNotes).run()
} }
runner.notifyListeners(command) runner.notifyListeners(command)

@ -21,6 +21,7 @@ package org.isoron.uhabits.core.models
data class Entry( data class Entry(
val timestamp: Timestamp, val timestamp: Timestamp,
val value: Int, val value: Int,
val notes: String = "",
) { ) {
companion object { companion object {
/** /**

@ -100,7 +100,7 @@ open class EntryList {
val intervals = buildIntervals(frequency, original) val intervals = buildIntervals(frequency, original)
snapIntervalsTogether(intervals) snapIntervalsTogether(intervals)
val computed = buildEntriesFromInterval(original, intervals) val computed = buildEntriesFromInterval(original, intervals)
computed.filter { it.value != UNKNOWN }.forEach { add(it) } computed.filter { it.value != UNKNOWN || it.notes.isNotEmpty() }.forEach { add(it) }
} }
} }

@ -68,17 +68,10 @@ data class Habit(
} }
} }
fun isFailedToday(): Boolean { fun isEnteredToday(): Boolean {
val today = DateUtils.getTodayWithOffset() val today = DateUtils.getTodayWithOffset()
val value = computedEntries.get(today).value val value = computedEntries.get(today).value
return if (isNumerical) { return value != Entry.UNKNOWN
when (targetType) {
NumericalHabitType.AT_LEAST -> value / 1000.0 < targetValue
NumericalHabitType.AT_MOST -> value / 1000.0 > targetValue
}
} else {
value == Entry.NO
}
} }
fun recompute() { fun recompute() {

@ -43,7 +43,7 @@ abstract class HabitList : Iterable<Habit> {
*/ */
constructor() { constructor() {
observable = ModelObservable() observable = ModelObservable()
filter = HabitMatcherBuilder().setArchivedAllowed(true).build() filter = HabitMatcher(isArchivedAllowed = true)
} }
protected constructor(filter: HabitMatcher) { protected constructor(filter: HabitMatcher) {

@ -22,19 +22,21 @@ data class HabitMatcher(
val isArchivedAllowed: Boolean = false, val isArchivedAllowed: Boolean = false,
val isReminderRequired: Boolean = false, val isReminderRequired: Boolean = false,
val isCompletedAllowed: Boolean = true, val isCompletedAllowed: Boolean = true,
val isEnteredAllowed: Boolean = true,
) { ) {
fun matches(habit: Habit): Boolean { fun matches(habit: Habit): Boolean {
if (!isArchivedAllowed && habit.isArchived) return false if (!isArchivedAllowed && habit.isArchived) return false
if (isReminderRequired && !habit.hasReminder()) return false if (isReminderRequired && !habit.hasReminder()) return false
if (!isCompletedAllowed && (habit.isCompletedToday() || habit.isFailedToday())) return false if (!isCompletedAllowed && habit.isCompletedToday()) return false
if (!isEnteredAllowed && habit.isEnteredToday()) return false
return true return true
} }
companion object { companion object {
@JvmField @JvmField
val WITH_ALARM = HabitMatcherBuilder() val WITH_ALARM = HabitMatcher(
.setArchivedAllowed(true) isArchivedAllowed = true,
.setReminderRequired(true) isReminderRequired = true,
.build() )
} }
} }

@ -1,48 +0,0 @@
/*
* 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.uhabits.core.models
class HabitMatcherBuilder {
private var archivedAllowed = false
private var reminderRequired = false
private var completedAllowed = true
fun build(): HabitMatcher {
return HabitMatcher(
isArchivedAllowed = archivedAllowed,
isReminderRequired = reminderRequired,
isCompletedAllowed = completedAllowed,
)
}
fun setArchivedAllowed(archivedAllowed: Boolean): HabitMatcherBuilder {
this.archivedAllowed = archivedAllowed
return this
}
fun setCompletedAllowed(completedAllowed: Boolean): HabitMatcherBuilder {
this.completedAllowed = completedAllowed
return this
}
fun setReminderRequired(reminderRequired: Boolean): HabitMatcherBuilder {
this.reminderRequired = reminderRequired
return this
}
}

@ -21,6 +21,7 @@ package org.isoron.uhabits.core.models
import org.isoron.platform.time.LocalDate import org.isoron.platform.time.LocalDate
import org.isoron.platform.time.LocalDate.Companion.DAY_LENGTH import org.isoron.platform.time.LocalDate.Companion.DAY_LENGTH
import org.isoron.uhabits.core.utils.DateFormats.Companion.getCSVDateFormat import org.isoron.uhabits.core.utils.DateFormats.Companion.getCSVDateFormat
import org.isoron.uhabits.core.utils.DateFormats.Companion.getDialogDateFormat
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar
import org.isoron.uhabits.core.utils.DateUtils.Companion.truncate import org.isoron.uhabits.core.utils.DateUtils.Companion.truncate
@ -82,6 +83,10 @@ data class Timestamp(var unixTime: Long) : Comparable<Timestamp> {
return day return day
} }
fun toDialogDateString(): String {
return getDialogDateFormat().format(Date(unixTime))
}
override fun toString(): String { override fun toString(): String {
return getCSVDateFormat().format(Date(unixTime)) return getCSVDateFormat().format(Date(unixTime))
} }

@ -41,12 +41,17 @@ class EntryRecord {
@field:Column @field:Column
var id: Long? = null var id: Long? = null
@field:Column
var notes: String? = null
fun copyFrom(entry: Entry) { fun copyFrom(entry: Entry) {
timestamp = entry.timestamp.unixTime timestamp = entry.timestamp.unixTime
value = entry.value value = entry.value
notes = entry.notes
} }
fun toEntry(): Entry { fun toEntry(): Entry {
return Entry(Timestamp(timestamp!!), value!!) val notes = notes ?: ""
return Entry(Timestamp(timestamp!!), value!!, notes)
} }
} }

@ -78,6 +78,11 @@ class HabitCardListCache @Inject constructor(
return data.checkmarks[habitId]!! return data.checkmarks[habitId]!!
} }
@Synchronized
fun getNoteIndicators(habitId: Long): BooleanArray {
return data.notesIndicators[habitId]!!
}
@Synchronized @Synchronized
fun hasNoHabit(): Boolean { fun hasNoHabit(): Boolean {
return allHabits.isEmpty return allHabits.isEmpty
@ -163,6 +168,7 @@ class HabitCardListCache @Inject constructor(
data.habits.removeAt(position) data.habits.removeAt(position)
data.idToHabit.remove(id) data.idToHabit.remove(id)
data.checkmarks.remove(id) data.checkmarks.remove(id)
data.notesIndicators.remove(id)
data.scores.remove(id) data.scores.remove(id)
listener.onItemRemoved(position) listener.onItemRemoved(position)
} }
@ -207,6 +213,7 @@ class HabitCardListCache @Inject constructor(
val habits: MutableList<Habit> val habits: MutableList<Habit>
val checkmarks: HashMap<Long?, IntArray> val checkmarks: HashMap<Long?, IntArray>
val scores: HashMap<Long?, Double> val scores: HashMap<Long?, Double>
val notesIndicators: HashMap<Long?, BooleanArray>
@Synchronized @Synchronized
fun copyCheckmarksFrom(oldData: CacheData) { fun copyCheckmarksFrom(oldData: CacheData) {
@ -217,6 +224,15 @@ class HabitCardListCache @Inject constructor(
} }
} }
@Synchronized
fun copyNoteIndicatorsFrom(oldData: CacheData) {
val empty = BooleanArray(checkmarkCount)
for (id in idToHabit.keys) {
if (oldData.notesIndicators.containsKey(id)) notesIndicators[id] =
oldData.notesIndicators[id]!! else notesIndicators[id] = empty
}
}
@Synchronized @Synchronized
fun copyScoresFrom(oldData: CacheData) { fun copyScoresFrom(oldData: CacheData) {
for (id in idToHabit.keys) { for (id in idToHabit.keys) {
@ -241,6 +257,7 @@ class HabitCardListCache @Inject constructor(
habits = LinkedList() habits = LinkedList()
checkmarks = HashMap() checkmarks = HashMap()
scores = HashMap() scores = HashMap()
notesIndicators = HashMap()
} }
} }
@ -271,6 +288,7 @@ class HabitCardListCache @Inject constructor(
newData.fetchHabits() newData.fetchHabits()
newData.copyScoresFrom(data) newData.copyScoresFrom(data)
newData.copyCheckmarksFrom(data) newData.copyCheckmarksFrom(data)
newData.copyNoteIndicatorsFrom(data)
val today = getTodayWithOffset() val today = getTodayWithOffset()
val dateFrom = today.minus(checkmarkCount - 1) val dateFrom = today.minus(checkmarkCount - 1)
if (runner != null) runner!!.publishProgress(this, -1) if (runner != null) runner!!.publishProgress(this, -1)
@ -280,10 +298,14 @@ class HabitCardListCache @Inject constructor(
if (targetId != null && targetId != habit.id) continue if (targetId != null && targetId != habit.id) continue
newData.scores[habit.id] = habit.scores[today].value newData.scores[habit.id] = habit.scores[today].value
val list: MutableList<Int> = ArrayList() val list: MutableList<Int> = ArrayList()
for ((_, value) in habit.computedEntries.getByInterval(dateFrom, today)) val notesIndicators: MutableList<Boolean> = ArrayList()
for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) {
list.add(value) list.add(value)
notesIndicators.add(note.isNotEmpty())
}
val entries = list.toTypedArray() val entries = list.toTypedArray()
newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries) newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries)
newData.notesIndicators[habit.id] = notesIndicators.toBooleanArray()
runner!!.publishProgress(this, position) runner!!.publishProgress(this, position)
} }
} }
@ -311,6 +333,7 @@ class HabitCardListCache @Inject constructor(
data.idToHabit[id] = habit data.idToHabit[id] = habit
data.scores[id] = newData.scores[id]!! data.scores[id] = newData.scores[id]!!
data.checkmarks[id] = newData.checkmarks[id]!! data.checkmarks[id] = newData.checkmarks[id]!!
data.notesIndicators[id] = newData.notesIndicators[id]!!
listener.onItemInserted(position) listener.onItemInserted(position)
} }
@ -338,14 +361,18 @@ class HabitCardListCache @Inject constructor(
private fun performUpdate(id: Long, position: Int) { private fun performUpdate(id: Long, position: Int) {
val oldScore = data.scores[id]!! val oldScore = data.scores[id]!!
val oldCheckmarks = data.checkmarks[id] val oldCheckmarks = data.checkmarks[id]
val oldNoteIndicators = data.notesIndicators[id]
val newScore = newData.scores[id]!! val newScore = newData.scores[id]!!
val newCheckmarks = newData.checkmarks[id]!! val newCheckmarks = newData.checkmarks[id]!!
val newNoteIndicators = newData.notesIndicators[id]!!
var unchanged = true var unchanged = true
if (oldScore != newScore) unchanged = false if (oldScore != newScore) unchanged = false
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false
if (!Arrays.equals(oldNoteIndicators, newNoteIndicators)) unchanged = false
if (unchanged) return if (unchanged) return
data.scores[id] = newScore data.scores[id] = newScore
data.checkmarks[id] = newCheckmarks data.checkmarks[id] = newCheckmarks
data.notesIndicators[id] = newNoteIndicators
listener.onItemChanged(position) listener.onItemChanged(position)
} }

@ -22,6 +22,8 @@ import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitType
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.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.tasks.ExportCSVTask import org.isoron.uhabits.core.tasks.ExportCSVTask
@ -47,14 +49,27 @@ open class ListHabitsBehavior @Inject constructor(
} }
fun onEdit(habit: Habit, timestamp: Timestamp?) { fun onEdit(habit: Habit, timestamp: Timestamp?) {
val entries = habit.computedEntries val entry = habit.computedEntries.get(timestamp!!)
val oldValue = entries.get(timestamp!!).value.toDouble() if (habit.type == HabitType.NUMERICAL) {
screen.showNumberPicker( val oldValue = entry.value.toDouble()
oldValue / 1000, screen.showNumberPicker(
habit.unit oldValue / 1000,
) { newValue: Double -> habit.unit,
val value = (newValue * 1000).roundToInt() entry.notes,
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value)) timestamp.toDialogDateString(),
) { newValue: Double, newNotes: String, ->
val value = (newValue * 1000).roundToInt()
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
}
} else {
screen.showCheckmarkDialog(
entry.value,
entry.notes,
timestamp.toDialogDateString(),
habit.color,
) { newValue, newNotes ->
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes))
}
} }
} }
@ -105,8 +120,9 @@ open class ListHabitsBehavior @Inject constructor(
} }
fun onToggle(habit: Habit, timestamp: Timestamp?, value: Int) { fun onToggle(habit: Habit, timestamp: Timestamp?, value: Int) {
val notes = habit.computedEntries.get(timestamp!!).notes
commandRunner.run( commandRunner.run(
CreateRepetitionCommand(habitList, habit, timestamp!!, value) CreateRepetitionCommand(habitList, habit, timestamp, value, notes)
) )
} }
@ -131,10 +147,15 @@ open class ListHabitsBehavior @Inject constructor(
} }
fun interface NumberPickerCallback { fun interface NumberPickerCallback {
fun onNumberPicked(newValue: Double) fun onNumberPicked(newValue: Double, notes: String)
fun onNumberPickerDismissed() {} fun onNumberPickerDismissed() {}
} }
fun interface CheckMarkDialogCallback {
fun onNotesSaved(value: Int, notes: String)
fun onNotesDismissed() {}
}
interface Screen { interface Screen {
fun showHabitScreen(h: Habit) fun showHabitScreen(h: Habit)
fun showIntroScreen() fun showIntroScreen()
@ -142,8 +163,17 @@ open class ListHabitsBehavior @Inject constructor(
fun showNumberPicker( fun showNumberPicker(
value: Double, value: Double,
unit: String, unit: String,
notes: String,
dateString: String,
callback: NumberPickerCallback callback: NumberPickerCallback
) )
fun showCheckmarkDialog(
value: Int,
notes: String,
dateString: String,
color: PaletteColor,
callback: CheckMarkDialogCallback
)
fun showSendBugReportToDeveloperScreen(log: String) fun showSendBugReportToDeveloperScreen(log: String)
fun showSendFileScreen(filename: String) fun showSendFileScreen(filename: String)

@ -20,7 +20,6 @@ package org.isoron.uhabits.core.ui.screens.habits.list
import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitMatcher import org.isoron.uhabits.core.models.HabitMatcher
import org.isoron.uhabits.core.models.HabitMatcherBuilder
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.ThemeSwitcher import org.isoron.uhabits.core.ui.ThemeSwitcher
import javax.inject.Inject import javax.inject.Inject
@ -33,6 +32,7 @@ class ListHabitsMenuBehavior @Inject constructor(
) { ) {
private var showCompleted: Boolean private var showCompleted: Boolean
private var showArchived: Boolean private var showArchived: Boolean
fun onCreateHabit() { fun onCreateHabit() {
screen.showSelectHabitTypeDialog() screen.showSelectHabitTypeDialog()
} }
@ -97,13 +97,26 @@ class ListHabitsMenuBehavior @Inject constructor(
screen.applyTheme() screen.applyTheme()
} }
fun onPreferencesChanged() {
updateAdapterFilter()
}
private fun updateAdapterFilter() { private fun updateAdapterFilter() {
adapter.setFilter( if (preferences.areQuestionMarksEnabled) {
HabitMatcherBuilder() adapter.setFilter(
.setArchivedAllowed(showArchived) HabitMatcher(
.setCompletedAllowed(showCompleted) isArchivedAllowed = showArchived,
.build() isEnteredAllowed = showCompleted,
) )
)
} else {
adapter.setFilter(
HabitMatcher(
isArchivedAllowed = showArchived,
isCompletedAllowed = showCompleted,
)
)
}
adapter.refresh() adapter.refresh()
} }

@ -46,6 +46,7 @@ data class HistoryCardState(
val firstWeekday: DayOfWeek, val firstWeekday: DayOfWeek,
val series: List<HistoryChart.Square>, val series: List<HistoryChart.Square>,
val defaultSquare: HistoryChart.Square, val defaultSquare: HistoryChart.Square,
val notesIndicators: List<Boolean>,
val theme: Theme, val theme: Theme,
val today: LocalDate, val today: LocalDate,
) )
@ -58,36 +59,74 @@ class HistoryCardPresenter(
val screen: Screen, val screen: Screen,
) : OnDateClickedListener { ) : OnDateClickedListener {
override fun onDateClicked(date: LocalDate) { override fun onDateLongPress(date: LocalDate) {
val timestamp = Timestamp.fromLocalDate(date) val timestamp = Timestamp.fromLocalDate(date)
screen.showFeedback() screen.showFeedback()
if (habit.isNumerical) { if (habit.isNumerical) {
val entries = habit.computedEntries showNumberPicker(timestamp)
val oldValue = entries.get(timestamp).value } else {
screen.showNumberPicker(oldValue / 1000.0, habit.unit) { newValue: Double -> val entry = habit.computedEntries.get(timestamp)
val thousands = (newValue * 1000).roundToInt() val nextValue = Entry.nextToggleValue(
value = entry.value,
isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
)
commandRunner.run(
CreateRepetitionCommand(
habitList,
habit,
timestamp,
nextValue,
entry.notes,
),
)
}
}
override fun onDateShortPress(date: LocalDate) {
val timestamp = Timestamp.fromLocalDate(date)
screen.showFeedback()
if (habit.isNumerical) {
showNumberPicker(timestamp)
} else {
val entry = habit.computedEntries.get(timestamp)
screen.showCheckmarkDialog(
entry.value,
entry.notes,
timestamp.toDialogDateString(),
preferences,
habit.color,
) { newValue, newNotes ->
commandRunner.run( commandRunner.run(
CreateRepetitionCommand( CreateRepetitionCommand(
habitList, habitList,
habit, habit,
timestamp, timestamp,
thousands, newValue,
newNotes,
), ),
) )
} }
} else { }
val currentValue = habit.computedEntries.get(timestamp).value }
val nextValue = Entry.nextToggleValue(
value = currentValue, private fun showNumberPicker(timestamp: Timestamp) {
isSkipEnabled = preferences.isSkipEnabled, val entry = habit.computedEntries.get(timestamp)
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled val oldValue = entry.value
) screen.showNumberPicker(
oldValue / 1000.0,
habit.unit,
entry.notes,
timestamp.toDialogDateString(),
) { newValue: Double, newNotes: String ->
val thousands = (newValue * 1000).roundToInt()
commandRunner.run( commandRunner.run(
CreateRepetitionCommand( CreateRepetitionCommand(
habitList, habitList,
habit, habit,
timestamp, timestamp,
nextValue, thousands,
newNotes,
), ),
) )
} }
@ -137,13 +176,21 @@ class HistoryCardPresenter(
else else
HistoryChart.Square.OFF HistoryChart.Square.OFF
val notesIndicators = entries.map {
when (it.notes) {
"" -> false
else -> true
}
}
return HistoryCardState( return HistoryCardState(
color = habit.color, color = habit.color,
firstWeekday = firstWeekday, firstWeekday = firstWeekday,
today = today.toLocalDate(), today = today.toLocalDate(),
theme = theme, theme = theme,
series = series, series = series,
defaultSquare = defaultSquare defaultSquare = defaultSquare,
notesIndicators = notesIndicators,
) )
} }
} }
@ -154,7 +201,17 @@ class HistoryCardPresenter(
fun showNumberPicker( fun showNumberPicker(
value: Double, value: Double,
unit: String, unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback, callback: ListHabitsBehavior.NumberPickerCallback,
) )
fun showCheckmarkDialog(
value: Int,
notes: String,
dateString: String,
preferences: Preferences,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback,
)
} }
} }

@ -33,8 +33,9 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.round import kotlin.math.round
fun interface OnDateClickedListener { interface OnDateClickedListener {
fun onDateClicked(date: LocalDate) fun onDateShortPress(date: LocalDate) {}
fun onDateLongPress(date: LocalDate) {}
} }
class HistoryChart( class HistoryChart(
@ -43,9 +44,10 @@ class HistoryChart(
var paletteColor: PaletteColor, var paletteColor: PaletteColor,
var series: List<Square>, var series: List<Square>,
var defaultSquare: Square, var defaultSquare: Square,
var notesIndicators: List<Boolean>,
var theme: Theme, var theme: Theme,
var today: LocalDate, var today: LocalDate,
var onDateClickedListener: OnDateClickedListener = OnDateClickedListener { }, var onDateClickedListener: OnDateClickedListener = object : OnDateClickedListener {},
var padding: Double = 0.0, var padding: Double = 0.0,
) : DataView { ) : DataView {
@ -73,6 +75,14 @@ class HistoryChart(
get() = squareSpacing + squareSize get() = squareSpacing + squareSize
override fun onClick(x: Double, y: Double) { override fun onClick(x: Double, y: Double) {
onDateClicked(x, y, false)
}
override fun onLongClick(x: Double, y: Double) {
onDateClicked(x, y, true)
}
private fun onDateClicked(x: Double, y: Double, isLongClick: Boolean) {
if (width <= 0.0) throw IllegalStateException("onClick must be called after draw(canvas)") if (width <= 0.0) throw IllegalStateException("onClick must be called after draw(canvas)")
val col = ((x - padding) / squareSize).toInt() val col = ((x - padding) / squareSize).toInt()
val row = ((y - padding) / squareSize).toInt() val row = ((y - padding) / squareSize).toInt()
@ -80,7 +90,11 @@ class HistoryChart(
if (row == 0 || col == nColumns) return if (row == 0 || col == nColumns) return
val clickedDate = topLeftDate.plus(offset) val clickedDate = topLeftDate.plus(offset)
if (clickedDate.isNewerThan(today)) return if (clickedDate.isNewerThan(today)) return
onDateClickedListener.onDateClicked(clickedDate) if (isLongClick) {
onDateClickedListener.onDateLongPress(clickedDate)
} else {
onDateClickedListener.onDateShortPress(clickedDate)
}
} }
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
@ -192,7 +206,9 @@ class HistoryChart(
) { ) {
val value = if (offset >= series.size) defaultSquare else series[offset] val value = if (offset >= series.size) defaultSquare else series[offset]
val hasNotes = if (offset >= notesIndicators.size) false else notesIndicators[offset]
val squareColor: Color val squareColor: Color
val circleColor: Color
val color = theme.color(paletteColor.paletteIndex) val color = theme.color(paletteColor.paletteIndex)
squareColor = when (value) { squareColor = when (value) {
Square.ON -> { Square.ON -> {
@ -236,5 +252,14 @@ class HistoryChart(
canvas.setColor(textColor) canvas.setColor(textColor)
canvas.setTextAlign(TextAlign.CENTER) canvas.setTextAlign(TextAlign.CENTER)
canvas.drawText(date.day.toString(), x + width / 2, y + width / 2) canvas.drawText(date.day.toString(), x + width / 2, y + width / 2)
if (hasNotes) {
circleColor = when (value) {
Square.ON -> theme.lowContrastTextColor
else -> color
}
canvas.setColor(circleColor)
canvas.fillCircle(x + width - width / 5, y + width / 5, width / 12)
}
} }
} }

@ -37,40 +37,45 @@ class WidgetBehavior @Inject constructor(
) { ) {
fun onAddRepetition(habit: Habit, timestamp: Timestamp?) { fun onAddRepetition(habit: Habit, timestamp: Timestamp?) {
notificationTray.cancel(habit) notificationTray.cancel(habit)
setValue(habit, timestamp, Entry.YES_MANUAL) val entry = habit.originalEntries.get(timestamp!!)
setValue(habit, timestamp, Entry.YES_MANUAL, entry.notes)
} }
fun onRemoveRepetition(habit: Habit, timestamp: Timestamp?) { fun onRemoveRepetition(habit: Habit, timestamp: Timestamp?) {
notificationTray.cancel(habit) notificationTray.cancel(habit)
setValue(habit, timestamp, Entry.NO) val entry = habit.originalEntries.get(timestamp!!)
setValue(habit, timestamp, Entry.NO, entry.notes)
} }
fun onToggleRepetition(habit: Habit, timestamp: Timestamp) { fun onToggleRepetition(habit: Habit, timestamp: Timestamp) {
val currentValue = habit.originalEntries.get(timestamp).value val entry = habit.originalEntries.get(timestamp)
val currentValue = entry.value
val newValue = nextToggleValue( val newValue = nextToggleValue(
value = currentValue, value = currentValue,
isSkipEnabled = preferences.isSkipEnabled, isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
) )
setValue(habit, timestamp, newValue) setValue(habit, timestamp, newValue, entry.notes)
notificationTray.cancel(habit) notificationTray.cancel(habit)
} }
fun onIncrement(habit: Habit, timestamp: Timestamp, amount: Int) { fun onIncrement(habit: Habit, timestamp: Timestamp, amount: Int) {
val currentValue = habit.computedEntries.get(timestamp).value val entry = habit.computedEntries.get(timestamp)
setValue(habit, timestamp, currentValue + amount) val currentValue = entry.value
setValue(habit, timestamp, currentValue + amount, entry.notes)
notificationTray.cancel(habit) notificationTray.cancel(habit)
} }
fun onDecrement(habit: Habit, timestamp: Timestamp, amount: Int) { fun onDecrement(habit: Habit, timestamp: Timestamp, amount: Int) {
val currentValue = habit.computedEntries.get(timestamp).value val entry = habit.computedEntries.get(timestamp)
setValue(habit, timestamp, currentValue - amount) val currentValue = entry.value
setValue(habit, timestamp, currentValue - amount, entry.notes)
notificationTray.cancel(habit) notificationTray.cancel(habit)
} }
fun setValue(habit: Habit, timestamp: Timestamp?, newValue: Int) { fun setValue(habit: Habit, timestamp: Timestamp?, newValue: Int, notes: String) {
commandRunner.run( commandRunner.run(
CreateRepetitionCommand(habitList, habit, timestamp!!, newValue) CreateRepetitionCommand(habitList, habit, timestamp!!, newValue, notes)
) )
} }
} }

@ -41,5 +41,8 @@ class DateFormats {
@JvmStatic fun getCSVDateFormat(): SimpleDateFormat = @JvmStatic fun getCSVDateFormat(): SimpleDateFormat =
fromSkeleton("yyyy-MM-dd", Locale.US) fromSkeleton("yyyy-MM-dd", Locale.US)
@JvmStatic fun getDialogDateFormat(): SimpleDateFormat =
fromSkeleton("MMM dd, yyyy", Locale.US)
} }
} }

@ -0,0 +1 @@
alter table Repetitions add column notes text;

@ -38,7 +38,7 @@ class CreateRepetitionCommandTest : BaseUnitTest() {
habit = fixtures.createShortHabit() habit = fixtures.createShortHabit()
habitList.add(habit) habitList.add(habit)
today = getToday() today = getToday()
command = CreateRepetitionCommand(habitList, habit, today, 100) command = CreateRepetitionCommand(habitList, habit, today, 100, "")
} }
@Test @Test

@ -34,6 +34,7 @@ import java.util.zip.ZipFile
class HabitsCSVExporterTest : BaseUnitTest() { class HabitsCSVExporterTest : BaseUnitTest() {
private lateinit var baseDir: File private lateinit var baseDir: File
@Before @Before
@Throws(Exception::class) @Throws(Exception::class)
override fun setUp() { override fun setUp() {
@ -41,12 +42,7 @@ class HabitsCSVExporterTest : BaseUnitTest() {
habitList.add(fixtures.createShortHabit()) habitList.add(fixtures.createShortHabit())
habitList.add(fixtures.createEmptyHabit()) habitList.add(fixtures.createEmptyHabit())
baseDir = Files.createTempDirectory("csv").toFile() baseDir = Files.createTempDirectory("csv").toFile()
} baseDir.deleteOnExit()
@Throws(Exception::class)
override fun tearDown() {
FileUtils.deleteDirectory(baseDir)
super.tearDown()
} }
@Test @Test
@ -63,14 +59,20 @@ class HabitsCSVExporterTest : BaseUnitTest() {
assertAbsolutePathExists(filename) assertAbsolutePathExists(filename)
val archive = File(filename) val archive = File(filename)
unzip(archive) unzip(archive)
assertPathExists("Habits.csv") val filesToCheck = arrayOf(
assertPathExists("001 Meditate/Checkmarks.csv") "001 Meditate/Checkmarks.csv",
assertPathExists("001 Meditate/Scores.csv") "001 Meditate/Scores.csv",
assertPathExists("002 Wake up early") "002 Wake up early/Checkmarks.csv",
assertPathExists("002 Wake up early/Checkmarks.csv") "002 Wake up early/Scores.csv",
assertPathExists("002 Wake up early/Scores.csv") "Checkmarks.csv",
assertPathExists("Checkmarks.csv") "Habits.csv",
assertPathExists("Scores.csv") "Scores.csv"
)
for (file in filesToCheck) {
assertPathExists(file)
assertFileAndReferenceAreEqual(file)
}
} }
@Throws(IOException::class) @Throws(IOException::class)
@ -104,4 +106,18 @@ class HabitsCSVExporterTest : BaseUnitTest() {
file.exists() file.exists()
) )
} }
private fun assertFileAndReferenceAreEqual(s: String) {
val assetFilename = String.format("csv_export/%s", s)
val file = File.createTempFile("asset", "")
file.deleteOnExit()
copyAssetToFile(assetFilename, file)
assertTrue(
FileUtils.contentEquals(
file,
File(String.format("%s/%s", baseDir.absolutePath, s))
)
)
}
} }

@ -54,6 +54,7 @@ class ImportTest : BaseUnitTest() {
assertTrue(isChecked(habit, 2016, 3, 18)) assertTrue(isChecked(habit, 2016, 3, 18))
assertTrue(isChecked(habit, 2016, 3, 19)) assertTrue(isChecked(habit, 2016, 3, 19))
assertFalse(isChecked(habit, 2016, 3, 20)) assertFalse(isChecked(habit, 2016, 3, 20))
assertTrue(isNotesEqual(habit, 2016, 3, 18, "text"))
} }
@Test @Test
@ -68,6 +69,8 @@ class ImportTest : BaseUnitTest() {
assertTrue(isChecked(habit, 2019, 4, 11)) assertTrue(isChecked(habit, 2019, 4, 11))
assertTrue(isChecked(habit, 2019, 5, 7)) assertTrue(isChecked(habit, 2019, 5, 7))
assertFalse(isChecked(habit, 2019, 6, 14)) assertFalse(isChecked(habit, 2019, 6, 14))
assertTrue(isNotesEqual(habit, 2019, 4, 11, "text"))
assertTrue(isNotesEqual(habit, 2019, 6, 14, "Habit 3 notes"))
} }
@Test @Test
@ -127,6 +130,13 @@ class ImportTest : BaseUnitTest() {
return h.originalEntries.get(timestamp).value == Entry.YES_MANUAL return h.originalEntries.get(timestamp).value == Entry.YES_MANUAL
} }
private fun isNotesEqual(h: Habit, year: Int, month: Int, day: Int, notes: String): Boolean {
val date = getStartOfTodayCalendar()
date.set(year, month - 1, day)
val timestamp = Timestamp(date)
return h.originalEntries.get(timestamp).notes == notes
}
@Throws(IOException::class) @Throws(IOException::class)
private fun importFromFile(assetFilename: String) { private fun importFromFile(assetFilename: String) {
val file = File.createTempFile("asset", "") val file = File.createTempFile("asset", "")

@ -53,12 +53,12 @@ class HabitListTest : BaseUnitTest() {
habitsArray[1].isArchived = true habitsArray[1].isArchived = true
habitsArray[4].isArchived = true habitsArray[4].isArchived = true
habitsArray[7].isArchived = true habitsArray[7].isArchived = true
activeHabits = habitList.getFiltered(HabitMatcherBuilder().build()) activeHabits = habitList.getFiltered(HabitMatcher())
reminderHabits = habitList.getFiltered( reminderHabits = habitList.getFiltered(
HabitMatcherBuilder() HabitMatcher(
.setArchivedAllowed(true) isArchivedAllowed = true,
.setReminderRequired(true) isReminderRequired = true,
.build() )
) )
} }
@ -181,10 +181,10 @@ class HabitListTest : BaseUnitTest() {
fun testOrder_inherit() { fun testOrder_inherit() {
habitList.primaryOrder = HabitList.Order.BY_COLOR_ASC habitList.primaryOrder = HabitList.Order.BY_COLOR_ASC
val filteredList = habitList.getFiltered( val filteredList = habitList.getFiltered(
HabitMatcherBuilder() HabitMatcher(
.setArchivedAllowed(false) isArchivedAllowed = false,
.setCompletedAllowed(false) isCompletedAllowed = false,
.build() )
) )
assertEquals(filteredList.primaryOrder, HabitList.Order.BY_COLOR_ASC) assertEquals(filteredList.primaryOrder, HabitList.Order.BY_COLOR_ASC)
} }

@ -82,12 +82,12 @@ class HabitTest : BaseUnitTest() {
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun test_isFailed() { fun test_isEntered() {
val h = modelFactory.buildHabit() val h = modelFactory.buildHabit()
assertFalse(h.isFailedToday()) assertFalse(h.isEnteredToday())
h.originalEntries.add(Entry(getToday(), Entry.NO)) h.originalEntries.add(Entry(getToday(), Entry.NO))
h.recompute() h.recompute()
assertTrue(h.isFailedToday()) assertTrue(h.isEnteredToday())
} }
@Test @Test
@ -119,35 +119,6 @@ class HabitTest : BaseUnitTest() {
assertTrue(h.isCompletedToday()) 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 @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testURI() { fun testURI() {

@ -28,7 +28,7 @@ import org.isoron.uhabits.core.database.Database
import org.isoron.uhabits.core.database.Repository import org.isoron.uhabits.core.database.Repository
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitMatcherBuilder import org.isoron.uhabits.core.models.HabitMatcher
import org.isoron.uhabits.core.models.ModelObservable import org.isoron.uhabits.core.models.ModelObservable
import org.isoron.uhabits.core.models.Reminder import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.core.models.WeekdayList
@ -69,12 +69,12 @@ class SQLiteHabitListTest : BaseUnitTest() {
habitsArray[4].isArchived = true habitsArray[4].isArchived = true
habitsArray[7].isArchived = true habitsArray[7].isArchived = true
habitList.update(habitsArray) habitList.update(habitsArray)
activeHabits = habitList.getFiltered(HabitMatcherBuilder().build()) activeHabits = habitList.getFiltered(HabitMatcher())
reminderHabits = habitList.getFiltered( reminderHabits = habitList.getFiltered(
HabitMatcherBuilder() HabitMatcher(
.setArchivedAllowed(true) isArchivedAllowed = true,
.setReminderRequired(true) isReminderRequired = true,
.build() )
) )
habitList.observable.addListener(listener) habitList.observable.addListener(listener)
} }

@ -70,7 +70,7 @@ class HabitCardListCacheTest : BaseUnitTest() {
@Test @Test
fun testCommandListener_single() { fun testCommandListener_single() {
val h2 = habitList.getByPosition(2) val h2 = habitList.getByPosition(2)
commandRunner.run(CreateRepetitionCommand(habitList, h2, today, Entry.NO)) commandRunner.run(CreateRepetitionCommand(habitList, h2, today, Entry.NO, ""))
verify(listener).onItemChanged(2) verify(listener).onItemChanged(2)
verify(listener).onRefreshFinished() verify(listener).onRefreshFinished()
verifyNoMoreInteractions(listener) verifyNoMoreInteractions(listener)

@ -79,8 +79,8 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
@Test @Test
fun testOnEdit() { fun testOnEdit() {
behavior.onEdit(habit2, getToday()) behavior.onEdit(habit2, getToday())
verify(screen).showNumberPicker(eq(0.1), eq("miles"), picker.capture()) verify(screen).showNumberPicker(eq(0.1), eq("miles"), eq(""), eq("Jan 25, 2015"), picker.capture())
picker.lastValue.onNumberPicked(100.0) picker.lastValue.onNumberPicked(100.0, "")
val today = getTodayWithOffset() val today = getTodayWithOffset()
assertThat(habit2.computedEntries.get(today).value, equalTo(100000)) assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
} }

@ -73,6 +73,10 @@ class HistoryChartTest {
1 -> DIMMED 1 -> DIMMED
else -> OFF else -> OFF
} }
},
notesIndicators = MutableList(85) {
index: Int ->
index % 3 == 0
} }
) )
@ -87,20 +91,20 @@ class HistoryChartTest {
// Click top left date // Click top left date
view.onClick(20.0, 46.0) view.onClick(20.0, 46.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, Month.OCTOBER, 26)) verify(dateClickedListener).onDateShortPress(LocalDate(2014, Month.OCTOBER, 26))
reset(dateClickedListener) reset(dateClickedListener)
view.onClick(2.0, 28.0) view.onClick(2.0, 28.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, Month.OCTOBER, 26)) verify(dateClickedListener).onDateShortPress(LocalDate(2014, Month.OCTOBER, 26))
reset(dateClickedListener) reset(dateClickedListener)
// Click date in the middle // Click date in the middle
view.onClick(163.0, 113.0) view.onClick(163.0, 113.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, Month.DECEMBER, 10)) verify(dateClickedListener).onDateShortPress(LocalDate(2014, Month.DECEMBER, 10))
reset(dateClickedListener) reset(dateClickedListener)
// Click today // Click today
view.onClick(336.0, 37.0) view.onClick(336.0, 37.0)
verify(dateClickedListener).onDateClicked(LocalDate(2015, Month.JANUARY, 25)) verify(dateClickedListener).onDateShortPress(LocalDate(2015, Month.JANUARY, 25))
reset(dateClickedListener) reset(dateClickedListener)
// Click header // Click header
@ -112,6 +116,37 @@ class HistoryChartTest {
verifyNoMoreInteractions(dateClickedListener) verifyNoMoreInteractions(dateClickedListener)
} }
@Test
fun testLongClick() = runBlocking {
assertRenders(400, 200, "$base/base.png", view)
// Click top left date
view.onLongClick(20.0, 46.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 10, 26))
reset(dateClickedListener)
view.onLongClick(2.0, 28.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 10, 26))
reset(dateClickedListener)
// Click date in the middle
view.onLongClick(163.0, 113.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2014, 12, 10))
reset(dateClickedListener)
// Click today
view.onLongClick(336.0, 37.0)
verify(dateClickedListener).onDateLongPress(LocalDate(2015, 1, 25))
reset(dateClickedListener)
// Click header
view.onLongClick(160.0, 15.0)
verifyNoMoreInteractions(dateClickedListener)
// Click right axis
view.onLongClick(360.0, 60.0)
verifyNoMoreInteractions(dateClickedListener)
}
@Test @Test
fun testDrawWeekDay() = runBlocking { fun testDrawWeekDay() = runBlocking {
view.firstWeekday = DayOfWeek.MONDAY view.firstWeekday = DayOfWeek.MONDAY

@ -58,7 +58,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
fun testOnAddRepetition() { fun testOnAddRepetition() {
behavior.onAddRepetition(habit, today) behavior.onAddRepetition(habit, today)
verify(commandRunner).run( verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, Entry.YES_MANUAL) CreateRepetitionCommand(habitList, habit, today, Entry.YES_MANUAL, "")
) )
verify(notificationTray).cancel(habit) verify(notificationTray).cancel(habit)
verifyZeroInteractions(preferences) verifyZeroInteractions(preferences)
@ -68,7 +68,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
fun testOnRemoveRepetition() { fun testOnRemoveRepetition() {
behavior.onRemoveRepetition(habit, today) behavior.onRemoveRepetition(habit, today)
verify(commandRunner).run( verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, Entry.NO) CreateRepetitionCommand(habitList, habit, today, Entry.NO, "")
) )
verify(notificationTray).cancel(habit) verify(notificationTray).cancel(habit)
verifyZeroInteractions(preferences) verifyZeroInteractions(preferences)
@ -94,7 +94,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
behavior.onToggleRepetition(habit, today) behavior.onToggleRepetition(habit, today)
verify(preferences).isSkipEnabled verify(preferences).isSkipEnabled
verify(commandRunner).run( verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, nextValue) CreateRepetitionCommand(habitList, habit, today, nextValue, "")
) )
verify(notificationTray).cancel( verify(notificationTray).cancel(
habit habit
@ -110,7 +110,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
habit.recompute() habit.recompute()
behavior.onIncrement(habit, today, 100) behavior.onIncrement(habit, today, 100)
verify(commandRunner).run( verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, 600) CreateRepetitionCommand(habitList, habit, today, 600, "")
) )
verify(notificationTray).cancel(habit) verify(notificationTray).cancel(habit)
verifyZeroInteractions(preferences) verifyZeroInteractions(preferences)
@ -123,7 +123,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
habit.recompute() habit.recompute()
behavior.onDecrement(habit, today, 100) behavior.onDecrement(habit, today, 100)
verify(commandRunner).run( verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, 400) CreateRepetitionCommand(habitList, habit, today, 400, "")
) )
verify(notificationTray).cancel(habit) verify(notificationTray).cancel(habit)
verifyZeroInteractions(preferences) verifyZeroInteractions(preferences)

@ -1,19 +0,0 @@
HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText
Breed dragons,with love and fire,Diet & Food,2016-03-18,1,
Breed dragons,with love and fire,Diet & Food,2016-03-19,1,
Breed dragons,with love and fire,Diet & Food,2016-03-21,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-15,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-16,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-17,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-21,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-15,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-16,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-18,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-21,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-15,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-16,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-18,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-21,1,
Grow spiritually,"transcend ego, practice compassion, smile and breath",Meditation,2016-03-15,1,
Grow spiritually,"transcend ego, practice compassion, smile and breath",Meditation,2016-03-17,1,
Grow spiritually,"transcend ego, practice compassion, smile and breath",Meditation,2016-03-21,1,
1 HabitName HabitDescription HabitCategory CalendarDate Value CommentText
2 Breed dragons with love and fire Diet & Food 2016-03-18 1
3 Breed dragons with love and fire Diet & Food 2016-03-19 1
4 Breed dragons with love and fire Diet & Food 2016-03-21 1
5 Reduce sleep only 2 hours per day Time Management 2016-03-15 1
6 Reduce sleep only 2 hours per day Time Management 2016-03-16 1
7 Reduce sleep only 2 hours per day Time Management 2016-03-17 1
8 Reduce sleep only 2 hours per day Time Management 2016-03-21 1
9 No-arms pushup Become like water my friend! Fitness 2016-03-15 1
10 No-arms pushup Become like water my friend! Fitness 2016-03-16 1
11 No-arms pushup Become like water my friend! Fitness 2016-03-18 1
12 No-arms pushup Become like water my friend! Fitness 2016-03-21 1
13 No-arms pushup Become like water my friend! Fitness 2016-03-15 1
14 No-arms pushup Become like water my friend! Fitness 2016-03-16 1
15 No-arms pushup Become like water my friend! Fitness 2016-03-18 1
16 No-arms pushup Become like water my friend! Fitness 2016-03-21 1
17 Grow spiritually transcend ego, practice compassion, smile and breath Meditation 2016-03-15 1
18 Grow spiritually transcend ego, practice compassion, smile and breath Meditation 2016-03-17 1
19 Grow spiritually transcend ego, practice compassion, smile and breath Meditation 2016-03-21 1

@ -22,7 +22,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins { plugins {
application application
id("kotlin") id("kotlin")
id("com.github.johnrengelman.shadow") version "7.0.0" id("com.github.johnrengelman.shadow") version "7.1.0"
} }
@ -33,9 +33,9 @@ application {
} }
dependencies { dependencies {
val ktorVersion = "1.6.4" val ktorVersion = "1.6.6"
val kotlinVersion = "1.5.31" val kotlinVersion = "1.6.0"
val logbackVersion = "1.2.6" val logbackVersion = "1.2.7"
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("io.ktor:ktor-server-netty:$ktorVersion") implementation("io.ktor:ktor-server-netty:$ktorVersion")
implementation("ch.qos.logback:logback-classic:$logbackVersion") implementation("ch.qos.logback:logback-classic:$logbackVersion")

Loading…
Cancel
Save