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

pull/1120/head
sgallese 4 years ago
commit b870dd85f4

@ -1,6 +1,6 @@
plugins {
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.kapt") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false)
@ -18,8 +18,6 @@ allprojects {
mavenCentral()
maven(url = "https://plugins.gradle.org/m2/")
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")
}
}

@ -18,7 +18,7 @@
*/
plugins {
id("com.github.triplet.play") version "3.6.0"
id("com.github.triplet.play") version "3.7.0"
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt")
@ -86,10 +86,10 @@ android {
}
dependencies {
val daggerVersion = "2.39"
val kotlinVersion = "1.5.31"
val daggerVersion = "2.40.3"
val kotlinVersion = "1.6.0"
val kxCoroutinesVersion = "1.5.2"
val ktorVersion = "1.6.4"
val ktorVersion = "1.6.6"
val espressoVersion = "3.4.0"
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
@ -98,7 +98,7 @@ dependencies {
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1")
androidTestImplementation("io.ktor:ktor-client-mock:$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.uiautomator:uiautomator:2.2.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
fun testClick_shortToggleDisabled() {
prefs.isShortToggleEnabled = false
view.performClick()
assertFalse(edited)
}
@Test
fun testClick_shortToggleEnabled() {
prefs.isShortToggleEnabled = true
fun testClick() {
view.performClick()
assertTrue(edited)
}

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

@ -49,23 +49,12 @@ class AndroidDataView(
override fun onShowPress(e: MotionEvent?) = Unit
override fun onSingleTapUp(e: MotionEvent?): 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
}
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
return true
return handleClick(e, true)
}
override fun onLongPress(e: MotionEvent?) = Unit
override fun onLongPress(e: MotionEvent?) {
handleClick(e)
}
override fun onScroll(
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 {
check(contentView.everyDayRadioButton)
unfocusAll()
}
contentView.everyXDaysRadioButton.setOnClickListener {
check(contentView.everyXDaysRadioButton)
val everyXDaysTextView = contentView.everyXDaysTextView
focus(everyXDaysTextView)
selectInputField(everyXDaysTextView)
}
contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
@ -83,7 +82,7 @@ class FrequencyPickerDialog(
contentView.xTimesPerWeekRadioButton.setOnClickListener {
check(contentView.xTimesPerWeekRadioButton)
focus(contentView.xTimesPerWeekTextView)
selectInputField(contentView.xTimesPerWeekTextView)
}
contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
@ -92,7 +91,7 @@ class FrequencyPickerDialog(
contentView.xTimesPerMonthRadioButton.setOnClickListener {
check(contentView.xTimesPerMonthRadioButton)
focus(contentView.xTimesPerMonthTextView)
selectInputField(contentView.xTimesPerMonthTextView)
}
contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
@ -101,7 +100,7 @@ class FrequencyPickerDialog(
contentView.xTimesPerYDaysRadioButton.setOnClickListener {
check(contentView.xTimesPerYDaysRadioButton)
focus(contentView.xTimesPerYDaysXTextView)
selectInputField(contentView.xTimesPerYDaysXTextView)
}
contentView.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus ->
@ -185,7 +184,7 @@ class FrequencyPickerDialog(
if (freqDenominator == 30 || freqDenominator == 31) {
contentView.xTimesPerMonthRadioButton.isChecked = true
contentView.xTimesPerMonthTextView.setText(freqNumerator.toString())
focus(contentView.xTimesPerMonthTextView)
selectInputField(contentView.xTimesPerMonthTextView)
} else {
if (freqNumerator == 1) {
if (freqDenominator == 1) {
@ -193,13 +192,13 @@ class FrequencyPickerDialog(
} else {
contentView.everyXDaysRadioButton.isChecked = true
contentView.everyXDaysTextView.setText(freqDenominator.toString())
focus(contentView.everyXDaysTextView)
selectInputField(contentView.everyXDaysTextView)
}
} else {
if (freqDenominator == 7) {
contentView.xTimesPerWeekRadioButton.isChecked = true
contentView.xTimesPerWeekTextView.setText(freqNumerator.toString())
focus(contentView.xTimesPerWeekTextView)
selectInputField(contentView.xTimesPerWeekTextView)
} else {
contentView.xTimesPerYDaysRadioButton.isChecked = true
contentView.xTimesPerYDaysXTextView.setText(freqNumerator.toString())
@ -209,8 +208,7 @@ class FrequencyPickerDialog(
}
}
private fun focus(view: EditText) {
view.requestFocus()
private fun selectInputField(view: EditText) {
view.setSelection(view.text.length)
}
@ -221,10 +219,4 @@ class FrequencyPickerDialog(
contentView.xTimesPerMonthRadioButton.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,
series = emptyList(),
defaultSquare = HistoryChart.Square.OFF,
notesIndicators = emptyList(),
theme = themeSwitcher.currentTheme,
today = DateUtils.getTodayWithOffset().toLocalDate(),
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
padding = 10.0,
)
dataView = AndroidDataView(context!!, null)
@ -103,6 +104,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
)
chart?.series = model.series
chart?.defaultSquare = model.defaultSquare
chart?.notesIndicators = model.notesIndicators
dataView.postInvalidate()
}

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

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

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

@ -25,6 +25,7 @@ import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy
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.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
@ -89,6 +90,7 @@ class ListHabitsScreen
private val importTaskFactory: ImportDataTaskFactory,
private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory,
private val checkMarkDialog: CheckmarkDialog,
private val behavior: Lazy<ListHabitsBehavior>
) : CommandRunner.Listener,
ListHabitsBehavior.Screen,
@ -225,9 +227,28 @@ class ListHabitsScreen
override fun showNumberPicker(
value: Double,
unit: String,
notes: String,
dateString: String,
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? {

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

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

@ -124,8 +124,9 @@ class HabitCardListAdapter @Inject constructor(
val habit = cache.getHabitByPosition(position)
val score = cache.getScore(habit!!.id!!)
val checkmarks = cache.getCheckmarks(habit.id!!)
val notesIndicators = cache.getNoteIndicators(habit.id!!)
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) {

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

@ -36,7 +36,6 @@ import android.widget.TextView
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R
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.ModelObservable
import org.isoron.uhabits.core.models.Timestamp
@ -58,6 +57,12 @@ class HabitCardViewFactory
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
}
data class DelayedToggle(
var habit: Habit,
var timestamp: Timestamp,
var value: Int
)
class HabitCardView(
@ActivityContext context: Context,
checkmarkPanelFactory: CheckmarkPanelViewFactory,
@ -116,12 +121,22 @@ class HabitCardView(
numberPanel.threshold = value
}
var notesIndicators
get() = checkmarkPanel.notesIndicators
set(values) {
checkmarkPanel.notesIndicators = values
numberPanel.notesIndicators = values
}
var checkmarkPanel: CheckmarkPanelView
private var numberPanel: NumberPanelView
private var innerFrame: LinearLayout
private var label: TextView
private var scoreRing: RingView
private var currentToggleTaskId = 0
private var queuedToggles = mutableListOf<DelayedToggle>()
init {
scoreRing = RingView(context).apply {
val thickness = dp(3f)
@ -145,11 +160,14 @@ class HabitCardView(
onToggle = { timestamp, value ->
triggerRipple(timestamp)
habit?.let {
{
behavior.onToggle(it, timestamp, value)
}.delay(TOGGLE_DELAY_MILLIS)
val taskId = queueToggle(it, timestamp, value);
{ runPendingToggles(taskId) }.delay(TOGGLE_DELAY_MILLIS)
}
}
onEdit = { timestamp ->
triggerRipple(timestamp)
habit?.let { behavior.onEdit(it, timestamp) }
}
}
numberPanel = numberPanelFactory.create().apply {
@ -184,6 +202,24 @@ class HabitCardView(
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() {
Handler(Looper.getMainLooper()).post {
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.utils.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.drawNotesIndicator
import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sres
import java.lang.Double.max
import java.text.DecimalFormat
@ -101,6 +101,11 @@ class NumberButtonView(
field = value
invalidate()
}
var hasNotes = false
set(value) {
field = value
invalidate()
}
var onEdit: () -> Unit = {}
private var drawer: Drawer = Drawer(context)
@ -111,8 +116,7 @@ class NumberButtonView(
}
override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) onEdit()
else showMessage(resources.getString(R.string.long_press_to_edit))
onEdit()
}
override fun onLongClick(v: View): Boolean {
@ -211,6 +215,8 @@ class NumberButtonView(
rect.offset(0f, 1.3f * em)
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
}
drawNotesIndicator(canvas, color, em, hasNotes)
}
}
}

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

@ -32,12 +32,14 @@ import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
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.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
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.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
@ -164,9 +166,29 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
override fun showNumberPicker(
value: Double,
unit: String,
notes: String,
dateString: String,
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) {

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

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

@ -22,7 +22,9 @@ package org.isoron.uhabits.utils
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.ColorDrawable
import android.os.Handler
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.dp(value: Float) = InterfaceUtils.dpToPixels(context, value)
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
get() = StyledResources(context)

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

@ -60,8 +60,8 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
SystemUtils.unlockScreen(this)
}
override fun onNumberPicked(newValue: Double) {
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt())
override fun onNumberPicked(newValue: Double, notes: String) {
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt(), notes)
widgetUpdater.updateWidgets()
finish()
}
@ -79,6 +79,8 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
numberPickerFactory.create(
entry.value / 1000.0,
data.habit.unit,
entry.notes,
today.toDialogDateString(),
this
).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
submit a pull request adding yourself here.
-->
<TextView style="@style/About.Item" android:text="Álinson Santos Xavier"/>
<TextView style="@style/About.Item" android:text="Quentin Hibon"/>
<TextView style="@style/About.Item" android:text="Oleg Ivashchenko"/>
<TextView style="@style/About.Item" android:text="Rechee Jozil"/>
<TextView style="@style/About.Item" android:text="Luboš Luňák"/>
<TextView style="@style/About.Item" android:text="Kristian Tashkov"/>
<TextView style="@style/About.Item" android:text="Victor Yu"/>
<TextView style="@style/About.Item" android:text="Денис (sciamano)"/>
<TextView style="@style/About.Item" android:text="Christoph Hennemann"/>
<TextView style="@style/About.Item" android:text="Joseph Tran"/>
<TextView style="@style/About.Item" android:text="Nikhil (regularcoder)"/>
<TextView style="@style/About.Item" android:text="Álinson S. Xavier (@iSoron)"/>
<TextView style="@style/About.Item" android:text="Quentin Hibon (@hiqua)"/>
<TextView style="@style/About.Item" android:text="Oleg Ivashchenko (@olegivo)"/>
<TextView style="@style/About.Item" android:text="Rechee Jozil (@recheej)"/>
<TextView style="@style/About.Item" android:text="Jakub Kalinowski (@kalina559)"/>
<TextView style="@style/About.Item" android:text="Luboš Luňák (@llunak)"/>
<TextView style="@style/About.Item" android:text="Sebastian Gallese (@sgallese)"/>
<TextView style="@style/About.Item" android:text="Kristian Tashkov (@KristianTashkov)"/>
<TextView style="@style/About.Item" android:text="Bindu (@vbh)"/>
<TextView style="@style/About.Item" android:text="Victor Yu (@vyu1)"/>
<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
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"
android:orientation="horizontal"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<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_width="wrap_content"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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="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
android:id="@+id/picker2"
android:layout_gravity="center"
android:layout_width="wrap_content"
</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>
<TextView
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="iconArrowUp" format="reference"/>
<attr name="iconArrowDown" format="reference"/>
<attr name="dialogFormLabelColor" format="reference"/>
<attr name="toolbarPopupTheme" format="reference"/>

@ -80,7 +80,7 @@
<string name="interval_always_ask">Always ask</string>
<string name="interval_custom">Custom...</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_send_feedback">Send feedback to developer</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="export">Export</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="unit">Unit</string>
<string name="target_type">Target Type</string>

@ -63,6 +63,7 @@
<item name="windowBackgroundColor">@color/grey_200</item>
<item name="android:textColorAlertDialogListItem">@color/grey_800</item>
<item name="singleLineTitle">false</item>
<item name="dialogFormLabelColor">@color/white</item>
</style>
<style name="AppBaseThemeDark" parent="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
@ -110,6 +111,7 @@
<item name="buttonBarPositiveButtonStyle">@style/DialogButtonStyle</item>
<item name="android:textColorAlertDialogListItem">@color/grey_100</item>
<item name="singleLineTitle">false</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style>
<style name="AppBaseThemeDark.PureBlack">
@ -130,6 +132,7 @@
<item name="textColorAlertDialogListItem">@color/grey_100</item>
<item name="windowBackgroundColor">@color/black</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material.PureBlack</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style>
<style name="BaseDialog" parent="Theme.AppCompat.Light.Dialog">
@ -140,6 +143,7 @@
<item name="contrast80">@color/grey_700</item>
<item name="contrast100">@color/grey_800</item>
<item name="palette">@array/lightPalette</item>
<item name="dialogFormLabelColor">@color/white</item>
</style>
<style name="BaseDialogDark" parent="Theme.AppCompat.Dialog">
@ -150,6 +154,7 @@
<item name="contrast80">@color/grey_300</item>
<item name="contrast100">@color/grey_100</item>
<item name="palette">@array/darkPalette</item>
<item name="dialogFormLabelColor">@color/grey_800</item>
</style>
<style name="PreferenceThemeOverlay.v14.Material.PureBlack">
@ -360,4 +365,38 @@
<item name="android:layout_height">1dp</item>
<item name="android:background">?attr/contrast20</item>
</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>

@ -27,7 +27,7 @@
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_short_toggle"
android:summary="@string/pref_toggle_description"
android:summary="@string/pref_toggle_description_2"
android:title="@string/pref_toggle_title"
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
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-21,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
H1,,C1,11/5/2020,1,
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/13/2019,0,
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/12/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,
H4,Habit 4,C4,11/6/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 {
dependencies {
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("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("androidx.annotation:annotation:1.2.0")
implementation("androidx.annotation:annotation:1.3.0")
implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.opencsv:opencsv:5.5.2")
implementation("commons-codec:commons-codec:1.15")

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

@ -20,4 +20,4 @@ package org.isoron.uhabits.core
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 timestamp: Timestamp,
val value: Int,
val notes: String,
) : Command {
override fun run() {
val entries = habit.originalEntries
entries.add(Entry(timestamp, value))
entries.add(Entry(timestamp, value, notes))
habit.recompute()
habitList.resort()
}

@ -76,8 +76,11 @@ class HabitBullCSVImporter
map[name] = h
logger.info("Creating habit: $name")
}
val notes = cols[5] ?: ""
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())
scoresWriter.write(sb.toString())
for (j in selectedHabits.indices) {
checksWriter.write(checkmarks[j][i].toString())
checksWriter.write(checkmarks[j][i].value.toString())
checksWriter.write(delimiter)
val score = String.format(Locale.US, "%.4f", scores[j][i].value)
scoresWriter.write(score)

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

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

@ -100,7 +100,7 @@ open class EntryList {
val intervals = buildIntervals(frequency, original)
snapIntervalsTogether(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 value = computedEntries.get(today).value
return if (isNumerical) {
when (targetType) {
NumericalHabitType.AT_LEAST -> value / 1000.0 < targetValue
NumericalHabitType.AT_MOST -> value / 1000.0 > targetValue
}
} else {
value == Entry.NO
}
return value != Entry.UNKNOWN
}
fun recompute() {

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

@ -22,19 +22,21 @@ data class HabitMatcher(
val isArchivedAllowed: Boolean = false,
val isReminderRequired: Boolean = false,
val isCompletedAllowed: Boolean = true,
val isEnteredAllowed: Boolean = true,
) {
fun matches(habit: Habit): Boolean {
if (!isArchivedAllowed && habit.isArchived) 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
}
companion object {
@JvmField
val WITH_ALARM = HabitMatcherBuilder()
.setArchivedAllowed(true)
.setReminderRequired(true)
.build()
val WITH_ALARM = HabitMatcher(
isArchivedAllowed = true,
isReminderRequired = true,
)
}
}

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

@ -41,12 +41,17 @@ class EntryRecord {
@field:Column
var id: Long? = null
@field:Column
var notes: String? = null
fun copyFrom(entry: Entry) {
timestamp = entry.timestamp.unixTime
value = entry.value
notes = entry.notes
}
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]!!
}
@Synchronized
fun getNoteIndicators(habitId: Long): BooleanArray {
return data.notesIndicators[habitId]!!
}
@Synchronized
fun hasNoHabit(): Boolean {
return allHabits.isEmpty
@ -163,6 +168,7 @@ class HabitCardListCache @Inject constructor(
data.habits.removeAt(position)
data.idToHabit.remove(id)
data.checkmarks.remove(id)
data.notesIndicators.remove(id)
data.scores.remove(id)
listener.onItemRemoved(position)
}
@ -207,6 +213,7 @@ class HabitCardListCache @Inject constructor(
val habits: MutableList<Habit>
val checkmarks: HashMap<Long?, IntArray>
val scores: HashMap<Long?, Double>
val notesIndicators: HashMap<Long?, BooleanArray>
@Synchronized
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
fun copyScoresFrom(oldData: CacheData) {
for (id in idToHabit.keys) {
@ -241,6 +257,7 @@ class HabitCardListCache @Inject constructor(
habits = LinkedList()
checkmarks = HashMap()
scores = HashMap()
notesIndicators = HashMap()
}
}
@ -271,6 +288,7 @@ class HabitCardListCache @Inject constructor(
newData.fetchHabits()
newData.copyScoresFrom(data)
newData.copyCheckmarksFrom(data)
newData.copyNoteIndicatorsFrom(data)
val today = getTodayWithOffset()
val dateFrom = today.minus(checkmarkCount - 1)
if (runner != null) runner!!.publishProgress(this, -1)
@ -280,10 +298,14 @@ class HabitCardListCache @Inject constructor(
if (targetId != null && targetId != habit.id) continue
newData.scores[habit.id] = habit.scores[today].value
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)
notesIndicators.add(note.isNotEmpty())
}
val entries = list.toTypedArray()
newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries)
newData.notesIndicators[habit.id] = notesIndicators.toBooleanArray()
runner!!.publishProgress(this, position)
}
}
@ -311,6 +333,7 @@ class HabitCardListCache @Inject constructor(
data.idToHabit[id] = habit
data.scores[id] = newData.scores[id]!!
data.checkmarks[id] = newData.checkmarks[id]!!
data.notesIndicators[id] = newData.notesIndicators[id]!!
listener.onItemInserted(position)
}
@ -338,14 +361,18 @@ class HabitCardListCache @Inject constructor(
private fun performUpdate(id: Long, position: Int) {
val oldScore = data.scores[id]!!
val oldCheckmarks = data.checkmarks[id]
val oldNoteIndicators = data.notesIndicators[id]
val newScore = newData.scores[id]!!
val newCheckmarks = newData.checkmarks[id]!!
val newNoteIndicators = newData.notesIndicators[id]!!
var unchanged = true
if (oldScore != newScore) unchanged = false
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false
if (!Arrays.equals(oldNoteIndicators, newNoteIndicators)) unchanged = false
if (unchanged) return
data.scores[id] = newScore
data.checkmarks[id] = newCheckmarks
data.notesIndicators[id] = newNoteIndicators
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.models.Habit
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.preferences.Preferences
import org.isoron.uhabits.core.tasks.ExportCSVTask
@ -47,14 +49,27 @@ open class ListHabitsBehavior @Inject constructor(
}
fun onEdit(habit: Habit, timestamp: Timestamp?) {
val entries = habit.computedEntries
val oldValue = entries.get(timestamp!!).value.toDouble()
screen.showNumberPicker(
oldValue / 1000,
habit.unit
) { newValue: Double ->
val value = (newValue * 1000).roundToInt()
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value))
val entry = habit.computedEntries.get(timestamp!!)
if (habit.type == HabitType.NUMERICAL) {
val oldValue = entry.value.toDouble()
screen.showNumberPicker(
oldValue / 1000,
habit.unit,
entry.notes,
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) {
val notes = habit.computedEntries.get(timestamp!!).notes
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 onNumberPicked(newValue: Double)
fun onNumberPicked(newValue: Double, notes: String)
fun onNumberPickerDismissed() {}
}
fun interface CheckMarkDialogCallback {
fun onNotesSaved(value: Int, notes: String)
fun onNotesDismissed() {}
}
interface Screen {
fun showHabitScreen(h: Habit)
fun showIntroScreen()
@ -142,8 +163,17 @@ open class ListHabitsBehavior @Inject constructor(
fun showNumberPicker(
value: Double,
unit: String,
notes: String,
dateString: String,
callback: NumberPickerCallback
)
fun showCheckmarkDialog(
value: Int,
notes: String,
dateString: String,
color: PaletteColor,
callback: CheckMarkDialogCallback
)
fun showSendBugReportToDeveloperScreen(log: 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.HabitMatcher
import org.isoron.uhabits.core.models.HabitMatcherBuilder
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.ThemeSwitcher
import javax.inject.Inject
@ -33,6 +32,7 @@ class ListHabitsMenuBehavior @Inject constructor(
) {
private var showCompleted: Boolean
private var showArchived: Boolean
fun onCreateHabit() {
screen.showSelectHabitTypeDialog()
}
@ -97,13 +97,26 @@ class ListHabitsMenuBehavior @Inject constructor(
screen.applyTheme()
}
fun onPreferencesChanged() {
updateAdapterFilter()
}
private fun updateAdapterFilter() {
adapter.setFilter(
HabitMatcherBuilder()
.setArchivedAllowed(showArchived)
.setCompletedAllowed(showCompleted)
.build()
)
if (preferences.areQuestionMarksEnabled) {
adapter.setFilter(
HabitMatcher(
isArchivedAllowed = showArchived,
isEnteredAllowed = showCompleted,
)
)
} else {
adapter.setFilter(
HabitMatcher(
isArchivedAllowed = showArchived,
isCompletedAllowed = showCompleted,
)
)
}
adapter.refresh()
}

@ -46,6 +46,7 @@ data class HistoryCardState(
val firstWeekday: DayOfWeek,
val series: List<HistoryChart.Square>,
val defaultSquare: HistoryChart.Square,
val notesIndicators: List<Boolean>,
val theme: Theme,
val today: LocalDate,
)
@ -58,36 +59,74 @@ class HistoryCardPresenter(
val screen: Screen,
) : OnDateClickedListener {
override fun onDateClicked(date: LocalDate) {
override fun onDateLongPress(date: LocalDate) {
val timestamp = Timestamp.fromLocalDate(date)
screen.showFeedback()
if (habit.isNumerical) {
val entries = habit.computedEntries
val oldValue = entries.get(timestamp).value
screen.showNumberPicker(oldValue / 1000.0, habit.unit) { newValue: Double ->
val thousands = (newValue * 1000).roundToInt()
showNumberPicker(timestamp)
} else {
val entry = habit.computedEntries.get(timestamp)
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(
CreateRepetitionCommand(
habitList,
habit,
timestamp,
thousands,
newValue,
newNotes,
),
)
}
} else {
val currentValue = habit.computedEntries.get(timestamp).value
val nextValue = Entry.nextToggleValue(
value = currentValue,
isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
)
}
}
private fun showNumberPicker(timestamp: Timestamp) {
val entry = habit.computedEntries.get(timestamp)
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(
CreateRepetitionCommand(
habitList,
habit,
timestamp,
nextValue,
thousands,
newNotes,
),
)
}
@ -137,13 +176,21 @@ class HistoryCardPresenter(
else
HistoryChart.Square.OFF
val notesIndicators = entries.map {
when (it.notes) {
"" -> false
else -> true
}
}
return HistoryCardState(
color = habit.color,
firstWeekday = firstWeekday,
today = today.toLocalDate(),
theme = theme,
series = series,
defaultSquare = defaultSquare
defaultSquare = defaultSquare,
notesIndicators = notesIndicators,
)
}
}
@ -154,7 +201,17 @@ class HistoryCardPresenter(
fun showNumberPicker(
value: Double,
unit: String,
notes: String,
dateString: String,
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.round
fun interface OnDateClickedListener {
fun onDateClicked(date: LocalDate)
interface OnDateClickedListener {
fun onDateShortPress(date: LocalDate) {}
fun onDateLongPress(date: LocalDate) {}
}
class HistoryChart(
@ -43,9 +44,10 @@ class HistoryChart(
var paletteColor: PaletteColor,
var series: List<Square>,
var defaultSquare: Square,
var notesIndicators: List<Boolean>,
var theme: Theme,
var today: LocalDate,
var onDateClickedListener: OnDateClickedListener = OnDateClickedListener { },
var onDateClickedListener: OnDateClickedListener = object : OnDateClickedListener {},
var padding: Double = 0.0,
) : DataView {
@ -73,6 +75,14 @@ class HistoryChart(
get() = squareSpacing + squareSize
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)")
val col = ((x - padding) / squareSize).toInt()
val row = ((y - padding) / squareSize).toInt()
@ -80,7 +90,11 @@ class HistoryChart(
if (row == 0 || col == nColumns) return
val clickedDate = topLeftDate.plus(offset)
if (clickedDate.isNewerThan(today)) return
onDateClickedListener.onDateClicked(clickedDate)
if (isLongClick) {
onDateClickedListener.onDateLongPress(clickedDate)
} else {
onDateClickedListener.onDateShortPress(clickedDate)
}
}
override fun draw(canvas: Canvas) {
@ -192,7 +206,9 @@ class HistoryChart(
) {
val value = if (offset >= series.size) defaultSquare else series[offset]
val hasNotes = if (offset >= notesIndicators.size) false else notesIndicators[offset]
val squareColor: Color
val circleColor: Color
val color = theme.color(paletteColor.paletteIndex)
squareColor = when (value) {
Square.ON -> {
@ -236,5 +252,14 @@ class HistoryChart(
canvas.setColor(textColor)
canvas.setTextAlign(TextAlign.CENTER)
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?) {
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?) {
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) {
val currentValue = habit.originalEntries.get(timestamp).value
val entry = habit.originalEntries.get(timestamp)
val currentValue = entry.value
val newValue = nextToggleValue(
value = currentValue,
isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
)
setValue(habit, timestamp, newValue)
setValue(habit, timestamp, newValue, entry.notes)
notificationTray.cancel(habit)
}
fun onIncrement(habit: Habit, timestamp: Timestamp, amount: Int) {
val currentValue = habit.computedEntries.get(timestamp).value
setValue(habit, timestamp, currentValue + amount)
val entry = habit.computedEntries.get(timestamp)
val currentValue = entry.value
setValue(habit, timestamp, currentValue + amount, entry.notes)
notificationTray.cancel(habit)
}
fun onDecrement(habit: Habit, timestamp: Timestamp, amount: Int) {
val currentValue = habit.computedEntries.get(timestamp).value
setValue(habit, timestamp, currentValue - amount)
val entry = habit.computedEntries.get(timestamp)
val currentValue = entry.value
setValue(habit, timestamp, currentValue - amount, entry.notes)
notificationTray.cancel(habit)
}
fun setValue(habit: Habit, timestamp: Timestamp?, newValue: Int) {
fun setValue(habit: Habit, timestamp: Timestamp?, newValue: Int, notes: String) {
commandRunner.run(
CreateRepetitionCommand(habitList, habit, timestamp!!, newValue)
CreateRepetitionCommand(habitList, habit, timestamp!!, newValue, notes)
)
}
}

@ -41,5 +41,8 @@ class DateFormats {
@JvmStatic fun getCSVDateFormat(): SimpleDateFormat =
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()
habitList.add(habit)
today = getToday()
command = CreateRepetitionCommand(habitList, habit, today, 100)
command = CreateRepetitionCommand(habitList, habit, today, 100, "")
}
@Test

@ -34,6 +34,7 @@ import java.util.zip.ZipFile
class HabitsCSVExporterTest : BaseUnitTest() {
private lateinit var baseDir: File
@Before
@Throws(Exception::class)
override fun setUp() {
@ -41,12 +42,7 @@ class HabitsCSVExporterTest : BaseUnitTest() {
habitList.add(fixtures.createShortHabit())
habitList.add(fixtures.createEmptyHabit())
baseDir = Files.createTempDirectory("csv").toFile()
}
@Throws(Exception::class)
override fun tearDown() {
FileUtils.deleteDirectory(baseDir)
super.tearDown()
baseDir.deleteOnExit()
}
@Test
@ -63,14 +59,20 @@ class HabitsCSVExporterTest : BaseUnitTest() {
assertAbsolutePathExists(filename)
val archive = File(filename)
unzip(archive)
assertPathExists("Habits.csv")
assertPathExists("001 Meditate/Checkmarks.csv")
assertPathExists("001 Meditate/Scores.csv")
assertPathExists("002 Wake up early")
assertPathExists("002 Wake up early/Checkmarks.csv")
assertPathExists("002 Wake up early/Scores.csv")
assertPathExists("Checkmarks.csv")
assertPathExists("Scores.csv")
val filesToCheck = arrayOf(
"001 Meditate/Checkmarks.csv",
"001 Meditate/Scores.csv",
"002 Wake up early/Checkmarks.csv",
"002 Wake up early/Scores.csv",
"Checkmarks.csv",
"Habits.csv",
"Scores.csv"
)
for (file in filesToCheck) {
assertPathExists(file)
assertFileAndReferenceAreEqual(file)
}
}
@Throws(IOException::class)
@ -104,4 +106,18 @@ class HabitsCSVExporterTest : BaseUnitTest() {
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, 19))
assertFalse(isChecked(habit, 2016, 3, 20))
assertTrue(isNotesEqual(habit, 2016, 3, 18, "text"))
}
@Test
@ -68,6 +69,8 @@ class ImportTest : BaseUnitTest() {
assertTrue(isChecked(habit, 2019, 4, 11))
assertTrue(isChecked(habit, 2019, 5, 7))
assertFalse(isChecked(habit, 2019, 6, 14))
assertTrue(isNotesEqual(habit, 2019, 4, 11, "text"))
assertTrue(isNotesEqual(habit, 2019, 6, 14, "Habit 3 notes"))
}
@Test
@ -127,6 +130,13 @@ class ImportTest : BaseUnitTest() {
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)
private fun importFromFile(assetFilename: String) {
val file = File.createTempFile("asset", "")

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

@ -82,12 +82,12 @@ class HabitTest : BaseUnitTest() {
@Test
@Throws(Exception::class)
fun test_isFailed() {
fun test_isEntered() {
val h = modelFactory.buildHabit()
assertFalse(h.isFailedToday())
assertFalse(h.isEnteredToday())
h.originalEntries.add(Entry(getToday(), Entry.NO))
h.recompute()
assertTrue(h.isFailedToday())
assertTrue(h.isEnteredToday())
}
@Test
@ -119,35 +119,6 @@ class HabitTest : BaseUnitTest() {
assertTrue(h.isCompletedToday())
}
@Test
@Throws(Exception::class)
fun test_isFailedNumerical() {
val h = modelFactory.buildHabit()
h.type = HabitType.NUMERICAL
h.targetType = NumericalHabitType.AT_LEAST
h.targetValue = 100.0
assertTrue(h.isFailedToday())
h.originalEntries.add(Entry(getToday(), 200000))
h.recompute()
assertFalse(h.isFailedToday())
h.originalEntries.add(Entry(getToday(), 100000))
h.recompute()
assertFalse(h.isFailedToday())
h.originalEntries.add(Entry(getToday(), 50000))
h.recompute()
assertTrue(h.isFailedToday())
h.targetType = NumericalHabitType.AT_MOST
h.originalEntries.add(Entry(getToday(), 200000))
h.recompute()
assertTrue(h.isFailedToday())
h.originalEntries.add(Entry(getToday(), 100000))
h.recompute()
assertFalse(h.isFailedToday())
h.originalEntries.add(Entry(getToday(), 50000))
h.recompute()
assertFalse(h.isFailedToday())
}
@Test
@Throws(Exception::class)
fun testURI() {

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

@ -70,7 +70,7 @@ class HabitCardListCacheTest : BaseUnitTest() {
@Test
fun testCommandListener_single() {
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).onRefreshFinished()
verifyNoMoreInteractions(listener)

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

@ -73,6 +73,10 @@ class HistoryChartTest {
1 -> DIMMED
else -> OFF
}
},
notesIndicators = MutableList(85) {
index: Int ->
index % 3 == 0
}
)
@ -87,20 +91,20 @@ class HistoryChartTest {
// Click top left date
view.onClick(20.0, 46.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, Month.OCTOBER, 26))
verify(dateClickedListener).onDateShortPress(LocalDate(2014, Month.OCTOBER, 26))
reset(dateClickedListener)
view.onClick(2.0, 28.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, Month.OCTOBER, 26))
verify(dateClickedListener).onDateShortPress(LocalDate(2014, Month.OCTOBER, 26))
reset(dateClickedListener)
// Click date in the middle
view.onClick(163.0, 113.0)
verify(dateClickedListener).onDateClicked(LocalDate(2014, Month.DECEMBER, 10))
verify(dateClickedListener).onDateShortPress(LocalDate(2014, Month.DECEMBER, 10))
reset(dateClickedListener)
// Click today
view.onClick(336.0, 37.0)
verify(dateClickedListener).onDateClicked(LocalDate(2015, Month.JANUARY, 25))
verify(dateClickedListener).onDateShortPress(LocalDate(2015, Month.JANUARY, 25))
reset(dateClickedListener)
// Click header
@ -112,6 +116,37 @@ class HistoryChartTest {
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
fun testDrawWeekDay() = runBlocking {
view.firstWeekday = DayOfWeek.MONDAY

@ -58,7 +58,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
fun testOnAddRepetition() {
behavior.onAddRepetition(habit, today)
verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, Entry.YES_MANUAL)
CreateRepetitionCommand(habitList, habit, today, Entry.YES_MANUAL, "")
)
verify(notificationTray).cancel(habit)
verifyZeroInteractions(preferences)
@ -68,7 +68,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
fun testOnRemoveRepetition() {
behavior.onRemoveRepetition(habit, today)
verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, Entry.NO)
CreateRepetitionCommand(habitList, habit, today, Entry.NO, "")
)
verify(notificationTray).cancel(habit)
verifyZeroInteractions(preferences)
@ -94,7 +94,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
behavior.onToggleRepetition(habit, today)
verify(preferences).isSkipEnabled
verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, nextValue)
CreateRepetitionCommand(habitList, habit, today, nextValue, "")
)
verify(notificationTray).cancel(
habit
@ -110,7 +110,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
habit.recompute()
behavior.onIncrement(habit, today, 100)
verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, 600)
CreateRepetitionCommand(habitList, habit, today, 600, "")
)
verify(notificationTray).cancel(habit)
verifyZeroInteractions(preferences)
@ -123,7 +123,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
habit.recompute()
behavior.onDecrement(habit, today, 100)
verify(commandRunner).run(
CreateRepetitionCommand(habitList, habit, today, 400)
CreateRepetitionCommand(habitList, habit, today, 400, "")
)
verify(notificationTray).cancel(habit)
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 {
application
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 {
val ktorVersion = "1.6.4"
val kotlinVersion = "1.5.31"
val logbackVersion = "1.2.6"
val ktorVersion = "1.6.6"
val kotlinVersion = "1.6.0"
val logbackVersion = "1.2.7"
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("io.ktor:ktor-server-netty:$ktorVersion")
implementation("ch.qos.logback:logback-classic:$logbackVersion")

Loading…
Cancel
Save