Merge branch 'iSoron:dev' into dev

pull/1356/head
Jakub Kalinowski 4 years ago committed by GitHub
commit 971f24ac17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -89,7 +89,7 @@ dependencies {
val daggerVersion = "2.41"
val kotlinVersion = "1.6.10"
val kxCoroutinesVersion = "1.6.0"
val ktorVersion = "1.6.7"
val ktorVersion = "1.6.8"
val espressoVersion = "3.4.0"
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")

@ -22,6 +22,7 @@ package org.isoron.uhabits.activities.common.dialogs
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.DialogInterface.BUTTON_NEGATIVE
import android.text.InputFilter
import android.text.Spanned
import android.view.LayoutInflater
@ -33,7 +34,11 @@ import android.widget.EditText
import android.widget.NumberPicker
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Frequency.Companion.DAILY
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils
@ -52,6 +57,7 @@ class NumberPickerFactory
unit: String,
notes: String,
dateString: String,
frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback
): AlertDialog {
val inflater = LayoutInflater.from(context)
@ -92,7 +98,7 @@ class NumberPickerFactory
picker2.value = intValue % 100
etNotes.setText(notes)
val dialog = AlertDialog.Builder(context)
val dialogBuilder = AlertDialog.Builder(context)
.setView(view)
.setTitle(dateString)
.setPositiveButton(R.string.save) { _, _ ->
@ -108,9 +114,24 @@ class NumberPickerFactory
.setOnDismissListener {
callback.onNumberPickerDismissed()
}
.create()
if (frequency == DAILY) {
dialogBuilder.setNegativeButton(R.string.skip_day) { _, _ ->
picker.clearFocus()
val v = Entry.SKIP.toDouble() / 1000
val note = etNotes.text.toString()
callback.onNumberPicked(v, note)
}
}
val dialog = dialogBuilder.create()
dialog.setOnShowListener {
val preferences =
(context.applicationContext as HabitsApplication).component.preferences
if (!preferences.isSkipEnabled) {
dialog.getButton(BUTTON_NEGATIVE).visibility = View.GONE
}
showSoftInput(dialog, pickerInputText)
}

@ -173,7 +173,8 @@ class FrequencyChart : ScrollableChart {
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
val i = localeWeekdayList[j] % 7
if (values != null) drawMarker(canvas, rect, values[i])
if (values != null)
drawMarker(canvas, rect, values[i])
rect.offset(0f, rowHeight)
}
drawFooter(canvas, rect, date)
@ -222,11 +223,14 @@ class FrequencyChart : ScrollableChart {
}
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) {
// value can be negative when the entry is skipped
val valueCopy = value?.let { max(0, it) }
val padding = rect!!.height() * 0.2f
// maximal allowed mark radius
val maxRadius = (rect.height() - 2 * padding) / 2.0f
// the real mark radius is scaled down by a factor depending on the maximal frequency
val scale = 1.0f / maxFreq * value!!
val scale = 1.0f / maxFreq * valueCopy!!
val radius = maxRadius * scale
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
pGraph!!.color = colors[colorIndex]

@ -40,6 +40,7 @@ import org.isoron.uhabits.core.commands.CreateHabitCommand
import org.isoron.uhabits.core.commands.DeleteHabitsCommand
import org.isoron.uhabits.core.commands.EditHabitCommand
import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.tasks.TaskRunner
@ -230,9 +231,10 @@ class ListHabitsScreen
unit: String,
notes: String,
dateString: String,
frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback
) {
numberPickerFactory.create(value, unit, notes, dateString, callback).show()
numberPickerFactory.create(value, unit, notes, dateString, frequency, callback).show()
}
override fun showCheckmarkDialog(

@ -29,6 +29,7 @@ import android.view.View
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
import org.isoron.uhabits.core.preferences.Preferences
@ -143,6 +144,12 @@ class NumberButtonView(
private val lowContrast: Int
private val mediumContrast: Int
private val paint = TextPaint().apply {
typeface = getFontAwesome()
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private val pUnit: TextPaint = TextPaint().apply {
textSize = getDimension(context, R.dimen.smallerTextSize)
typeface = NORMAL_TYPEFACE
@ -176,6 +183,11 @@ class NumberButtonView(
val textSize: Float
when {
value == Entry.SKIP.toDouble() / 1000 -> {
label = resources.getString(R.string.fa_skipped)
textSize = dim(R.dimen.smallTextSize)
typeface = getFontAwesome()
}
value >= 0 -> {
label = value.toShortString()
typeface = BOLD_TYPEFACE

@ -39,6 +39,7 @@ 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.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
@ -169,9 +170,10 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback,
frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback
) {
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, callback).show()
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, frequency, callback).show()
}
override fun showCheckmarkDialog(

@ -81,6 +81,7 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
data.habit.unit,
entry.notes,
today.toDialogDateString(),
data.habit.frequency,
this
).show()
}

@ -225,6 +225,7 @@
<string name="increment">Increment</string>
<string name="decrement">Decrement</string>
<string name="pref_skip_title">Enable skip days</string>
<string name="skip_day">Skip</string>
<string name="pref_skip_description">Toggle twice to add a skip instead of a checkmark. Skips keep your score unchanged and don\'t break your streak.</string>
<string name="pref_unknown_title">Show question marks for missing data</string>
<string name="pref_unknown_description">Differentiate days without data from actual lapses. To enter a lapse, toggle twice.</string>

@ -276,6 +276,8 @@ open class EntryList {
* For numerical habits, non-positive entry values are converted to zero. For boolean habits, each
* YES_MANUAL value is converted to 1000 and all other values are converted to zero.
*
* SKIP values are converted to zero (if they weren't, each SKIP day would count as 0.003).
*
* The returned list is sorted by timestamp, with the newest entry coming first and the oldest entry
* coming last. If the original list has gaps in it (for example, weeks or months without any
* entries), then the list produced by this method will also have gaps.
@ -289,6 +291,9 @@ fun List<Entry>.groupedSum(
): List<Entry> {
return this.map { (timestamp, value) ->
if (isNumerical) {
if (value == SKIP)
Entry(timestamp, 0)
else
Entry(timestamp, max(0, value))
} else {
Entry(timestamp, if (value == YES_MANUAL) 1000 else 0)
@ -304,3 +309,28 @@ fun List<Entry>.groupedSum(
-timestamp.unixTime
}
}
/**
* Counts the number of days with vaLue SKIP in the given period.
*/
fun List<Entry>.countSkippedDays(
truncateField: DateUtils.TruncateField,
firstWeekday: Int = Calendar.SATURDAY
): List<Entry> {
return this.map { (timestamp, value) ->
if (value == SKIP) {
Entry(timestamp, 1)
} else {
Entry(timestamp, 0)
}
}.groupBy { entry ->
entry.timestamp.truncate(
truncateField,
firstWeekday,
)
}.entries.map { (timestamp, entries) ->
Entry(timestamp, entries.sumOf { it.value })
}.sortedBy { (timestamp, _) ->
-timestamp.unixTime
}
}

@ -100,6 +100,7 @@ class ScoreList {
}
val normalizedRollingSum = rollingSum / 1000
if (values[offset] != Entry.SKIP) {
val percentageCompleted = if (!isAtMost) {
if (targetValue > 0)
min(1.0, normalizedRollingSum / targetValue)
@ -107,13 +108,17 @@ class ScoreList {
1.0
} else {
if (targetValue > 0) {
(1 - ((normalizedRollingSum - targetValue) / targetValue)).coerceIn(0.0, 1.0)
(1 - ((normalizedRollingSum - targetValue) / targetValue)).coerceIn(
0.0,
1.0
)
} else {
if (normalizedRollingSum > 0) 0.0 else 1.0
}
}
previousValue = compute(freq, previousValue, percentageCompleted)
}
} else {
if (values[offset] == Entry.YES_MANUAL) {
rollingSum += 1.0

@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.list
import org.isoron.platform.time.LocalDate
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitType
@ -58,6 +59,7 @@ open class ListHabitsBehavior @Inject constructor(
habit.unit,
entry.notes,
timestamp.toDialogDateString(),
habit.frequency
) { newValue: Double, newNotes: String, ->
val value = (newValue * 1000).roundToInt()
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
@ -167,6 +169,7 @@ open class ListHabitsBehavior @Inject constructor(
unit: String,
notes: String,
dateString: String,
frequency: Frequency,
callback: NumberPickerCallback
)
fun showCheckmarkDialog(

@ -27,6 +27,7 @@ import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
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.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
@ -123,6 +124,7 @@ class HistoryCardPresenter(
habit.unit,
entry.notes,
timestamp.toDialogDateString(),
frequency = habit.frequency
) { newValue: Double, newNotes: String ->
val thousands = (newValue * 1000).roundToInt()
commandRunner.run(
@ -154,6 +156,7 @@ class HistoryCardPresenter(
entries.map {
when {
it.value == Entry.UNKNOWN -> OFF
it.value == SKIP -> HATCHED
(habit.targetType == AT_MOST) && (it.value / 1000.0 <= habit.targetValue) -> ON
(habit.targetType == AT_LEAST) && (it.value / 1000.0 >= habit.targetValue) -> ON
else -> GREY
@ -196,8 +199,10 @@ class HistoryCardPresenter(
unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback,
frequency: Frequency,
callback: ListHabitsBehavior.NumberPickerCallback
)
fun showCheckmarkDialog(
selectedValue: Int,
notes: String,

@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.countSkippedDays
import org.isoron.uhabits.core.models.groupedSum
import org.isoron.uhabits.core.ui.views.Theme
import org.isoron.uhabits.core.utils.DateUtils
@ -51,37 +52,59 @@ class TargetCardPresenter {
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val skippedDayToday = entries.countSkippedDays(
truncateField = DateUtils.TruncateField.DAY
).firstOrNull()?.value ?: 0
val valueThisWeek = entries.groupedSum(
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val skippedDaysThisWeek = entries.countSkippedDays(
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
firstWeekday = firstWeekday
).firstOrNull()?.value ?: 0
val valueThisMonth = entries.groupedSum(
truncateField = DateUtils.TruncateField.MONTH,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val skippedDaysThisMonth = entries.countSkippedDays(
truncateField = DateUtils.TruncateField.MONTH,
).firstOrNull()?.value ?: 0
val valueThisQuarter = entries.groupedSum(
truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val skippedDaysThisQuarter = entries.countSkippedDays(
truncateField = DateUtils.TruncateField.QUARTER
).firstOrNull()?.value ?: 0
val valueThisYear = entries.groupedSum(
truncateField = DateUtils.TruncateField.YEAR,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val skippedDaysThisYear = entries.countSkippedDays(
truncateField = DateUtils.TruncateField.YEAR
).firstOrNull()?.value ?: 0
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
val daysInQuarter = 91
val daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR)
val targetToday = habit.targetValue / habit.frequency.denominator
val targetThisWeek = targetToday * 7
val targetThisMonth = targetToday * daysInMonth
val targetThisQuarter = targetToday * daysInQuarter
val targetThisYear = targetToday * daysInYear
val dailyTarget = habit.targetValue / habit.frequency.denominator
val targetToday = dailyTarget * (1 - skippedDayToday)
val targetThisWeek = dailyTarget * (7 - skippedDaysThisWeek)
val targetThisMonth = dailyTarget * (daysInMonth - skippedDaysThisMonth)
val targetThisQuarter = dailyTarget * (daysInQuarter - skippedDaysThisQuarter)
val targetThisYear = dailyTarget * (daysInYear - skippedDaysThisYear)
val values = ArrayList<Double>()
if (habit.frequency.denominator <= 1) values.add(valueToday / 1e3)

@ -23,6 +23,7 @@ import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.number.IsCloseTo
import org.hamcrest.number.OrderingComparison
import org.isoron.uhabits.core.BaseUnitTest
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
import org.junit.Before
import org.junit.Test
@ -381,6 +382,66 @@ class NumericalAtLeastScoreListTest : NumericalScoreListTest() {
}
}
class NumericalAtLeastScoreListWithSkipTest : NumericalScoreListTest() {
@Before
@Throws(Exception::class)
override fun setUp() {
super.setUp()
habit = fixtures.createEmptyNumericalHabit(NumericalHabitType.AT_LEAST)
}
@Test
fun test_getValue() {
addEntries(0, 10, 2000)
addEntries(10, 11, SKIP)
addEntries(11, 15, 2000)
addEntries(15, 16, SKIP)
addEntries(16, 20, 2000)
val expectedValues = doubleArrayOf(
0.617008,
0.596033,
0.573910,
0.550574,
0.525961,
0.500000,
0.472617,
0.443734,
0.413270,
0.381137,
0.347244, // skipped day should have the same score as the previous day
0.347244,
0.311495,
0.273788,
0.234017,
0.192067, // skipped day should have the same score as the previous day
0.192067,
0.147820,
0.101149,
0.051922,
0.000000,
0.000000,
0.000000
)
checkScoreValues(expectedValues)
}
@Test
fun skipsShouldNotAffectScore() {
addEntries(0, 500, 1000)
val initialScore = habit.scores[today].value
addEntries(500, 1000, SKIP)
assertThat(habit.scores[today].value, IsCloseTo.closeTo(initialScore, E))
addEntries(0, 300, 1000)
addEntries(300, 500, SKIP)
addEntries(500, 700, 1000)
// skipped days should be treated as if they never existed
assertThat(habit.scores[today].value, IsCloseTo.closeTo(initialScore, E))
}
}
class NumericalAtMostScoreListTest : NumericalScoreListTest() {
@Before
@Throws(Exception::class)

@ -33,6 +33,7 @@ import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.core.IsEqual.equalTo
import org.isoron.uhabits.core.BaseUnitTest
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
@ -79,7 +80,14 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
@Test
fun testOnEdit() {
behavior.onEdit(habit2, getToday())
verify(screen).showNumberPicker(eq(0.1), eq("miles"), eq(""), eq("Jan 25, 2015"), picker.capture())
verify(screen).showNumberPicker(
eq(0.1),
eq("miles"),
eq(""),
eq("Jan 25, 2015"),
eq(Frequency.DAILY),
picker.capture()
)
picker.lastValue.onNumberPicked(100.0, "")
val today = getTodayWithOffset()
assertThat(habit2.computedEntries.get(today).value, equalTo(100000))

@ -190,6 +190,7 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener
@objc func onMoreActionsClicked() {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
if isThereAnyArchivedHabit() {
if preferences.showArchived {
alert.addAction(UIAlertAction(title: strings.hide_archived, style: .default) {
(action: UIAlertAction) -> Void in
@ -203,6 +204,7 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener
self.dataSource.requestData()
})
}
}
if preferences.showCompleted {
alert.addAction(UIAlertAction(title: strings.hide_completed, style: .default) {
@ -262,4 +264,8 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener
let sections = NSIndexSet(indexesIn: NSMakeRange(0, self.tableView.numberOfSections))
tableView.reloadSections(sections as IndexSet, with: .automatic)
}
func isThereAnyArchivedHabit() -> Bool {
return data!.habits.filter({ $0.isArchived }).count > 0
}
}

@ -33,9 +33,9 @@ application {
}
dependencies {
val ktorVersion = "1.6.7"
val ktorVersion = "1.6.8"
val kotlinVersion = "1.6.10"
val logbackVersion = "1.2.10"
val logbackVersion = "1.2.11"
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