Compare commits

..

20 Commits

Author SHA1 Message Date
Alinson S. Xavier 516bf394f8 Merge branch 'release/2.3.1'
1 month ago
Alinson S. Xavier 2816b7c3d0 Bump version to 2.3.1 and update changelog
1 month ago
Alinson S. Xavier a9acbd6cab HabitCardListView: Prevent duplicate inset decorations
3 months ago
Alinson S. Xavier e121f46b61 Confetti: Fix position in freeform and landscape modes
3 months ago
Alinson S. Xavier d57de9d10c Apply bottom insets to about and settings screens
3 months ago
Alinson S. Xavier e4348a2144 Prevent some views from being obscured by system UI
3 months ago
Alinson S. Xavier e608c6ea62 Trim unit labels when necessary
3 months ago
Alinson S. Xavier 5403b6bd51 CheckmarkWidgetViewTest: Ignore non-deterministic test case
3 months ago
Alinson S. Xavier a6cf43dbca Reformat source code
3 months ago
Alinson S. Xavier 074627f6e1 Disable confetti if animations are disabled globally
3 months ago
powerjungle 96e20f751f Fix(UI): X and ? symbols easier to distinguish in "pure black" dark mode
3 months ago
Alinson S. Xavier 0daa4f6a2f Format source code
3 months ago
Alinson S. Xavier 035b392ece CSV export: Use formatted values, add notes & header
3 months ago
Alinson S. Xavier 648c7277cf CSV export: Add more fields to Habits.csv
3 months ago
Alinson S. Xavier 5006f5128b Update JVM target and toolchain to version 17
3 months ago
Alinson S. Xavier 97b98a872d EmptyListViewTest: Ignore non-deterministic test failures
3 months ago
Alinson S. Xavier 862a851e1c EmptyListViewTest: Remove unused imports
3 months ago
Alinson S. Xavier 804030f5c0 EmptyListViewTest: Instantiate view within each test method
3 months ago
Alinson S. Xavier 08ab3c22ce Merge branch 'master' into dev
3 months ago
Alinson S. Xavier b58f836d8e Merge branch 'release/2.3.0' into dev
3 months ago

@ -1,32 +1,54 @@
# Changelog # Changelog
## [2.3.1] -- 2025-08-13
### Changed
- Add notes to exported CSV files (@iSoron)
### Fixed
- Prevent some views from being obscured by system UI (@iSoron, #2171)
- Disable confetti if animations are disabled globally (@iSoron, #2170)
- Make symbols easier to distinguish in "pure black" dark mode (powerjungle, #2136)
- Trim unit labels when necessary (@hiqua, @iSoron, #2158)
## [2.3.0] -- 2025-06-23 ## [2.3.0] -- 2025-06-23
### Added ### Added
- Add support for Android 15 and 16 (@iSoron) - Add support for Android 15 and 16 (@iSoron)
- Show confetti animation (@gokulk16, @iSoron, #1743) - Show confetti animation (@gokulk16, @iSoron, #1743)
- Show streaks for measurable habits (@teckwarz, #2059) - Show streaks for measurable habits (@teckwarz, #2059)
- Allow user to unset measurable habits (@leontodd, @kalina559, #1899, #2109) - Allow user to unset measurable habits (@leontodd, @kalina559, #1899, #2109)
### Changed ### Changed
- Change background widget color for habits with implicit checks (@wobbba, #1915) - Change background widget color for habits with implicit checks (@wobbba, #1915)
### Fixed ### Fixed
- Fix notification when goal type is set to maximum (@manish99verma, #1931) - Fix notification when goal type is set to maximum (@manish99verma, #1931)
- Never mark "at most" habits as completed for the day (@kalina559, #2077) - Never mark "at most" habits as completed for the day (@kalina559, #2077)
- Increase minimum widget size (@iSoron, #2118) - Increase minimum widget size (@iSoron, #2118)
- Improve Gradle configuration (@jimlyas, #2108) - Improve Gradle configuration (@jimlyas, #2108)
## [2.2.0] -- 2024-01-30 ## [2.2.0] -- 2024-01-30
### Added ### Added
- Add support for Android 14 (@iSoron, @hiqua) - Add support for Android 14 (@iSoron, @hiqua)
- Allow user to change app language (@leondzn) - Allow user to change app language (@leondzn)
### Fixed ### Fixed
- Implement workaround to make notifications non-dismissible in Android 14 (@iSoron, #1872) - Implement workaround to make notifications non-dismissible in Android 14 (@iSoron, #1872)
- Fix splash screen background color in dark mode (@SIKV, #1888) - Fix splash screen background color in dark mode (@SIKV, #1888)
## [2.1.3] -- 2023-08-28 ## [2.1.3] -- 2023-08-28
### Fixed ### Fixed
- Use text input on Samsung devices (@iSoron, #1719) - Use text input on Samsung devices (@iSoron, #1719)
- Prevent crash if alarm permission is revoked (@iSoron) - Prevent crash if alarm permission is revoked (@iSoron)
- Adjust widget colors (@iSoron) - Adjust widget colors (@iSoron)
@ -34,7 +56,9 @@
- Fix skip button in locales that use comma instead of dot (@iSoron, #1721) - Fix skip button in locales that use comma instead of dot (@iSoron, #1721)
## [2.1.2] -- 2023-05-26 ## [2.1.2] -- 2023-05-26
### Fixed ### Fixed
- Fix bug that caused widget to enter checkmark on wrong date (@iSoron, #1541) - Fix bug that caused widget to enter checkmark on wrong date (@iSoron, #1541)
- Fix widget corners on Android 12 (@iSoron) - Fix widget corners on Android 12 (@iSoron)
- Fix bug that caused notes to be lost when editing a checkmark (@iSoron, #1566) - Fix bug that caused notes to be lost when editing a checkmark (@iSoron, #1566)
@ -42,18 +66,23 @@
- Accept comma (instead of dot) in certain locales (@iSoron) - Accept comma (instead of dot) in certain locales (@iSoron)
### Changed ### Changed
- Remove update delay after entering a checkmark (@iSoron) - Remove update delay after entering a checkmark (@iSoron)
### Removed ### Removed
- Remove stack widgets (@iSoron)
- Remove stack widgets (@iSoron)
## [2.1.1] -- 2022-09-24 ## [2.1.1] -- 2022-09-24
### Fixed ### Fixed
- Fix Tasker plugin (@iSoron, #1503) - Fix Tasker plugin (@iSoron, #1503)
## [2.1.0] -- 2022-09-10 ## [2.1.0] -- 2022-09-10
### Added ### Added
- Allow user to add notes to specific dates (@vbh, #1103) - Allow user to add notes to specific dates (@vbh, #1103)
- Allow user to track "at most" numerical habits (@KristianTashkov, #1101) - Allow user to track "at most" numerical habits (@KristianTashkov, #1101)
- Allow user to add skips to measurable habits (@kalina559, #1319) - Allow user to add skips to measurable habits (@kalina559, #1319)
@ -64,10 +93,12 @@
- Add support for Android 13 themed icons (@cheeeeer, #1497) - Add support for Android 13 themed icons (@cheeeeer, #1497)
### Removed ### Removed
- Hide snooze button Android 12 notifications (@hiqua, #1226) - Hide snooze button Android 12 notifications (@hiqua, #1226)
- Remove preference to set LED lights (@iSoron) - Remove preference to set LED lights (@iSoron)
### Changed ### Changed
- Hide failed habits along with completed ones (@hiqua, #1052) - Hide failed habits along with completed ones (@hiqua, #1052)
- Cycle through all checkmark states when toggling (@iSoron) - Cycle through all checkmark states when toggling (@iSoron)
- Add delay after toggling a habit (@hiqua, @kalina559, #1147) - Add delay after toggling a habit (@hiqua, @kalina559, #1147)
@ -76,6 +107,7 @@
- Increase target SDK to 31 (@hiqua) - Increase target SDK to 31 (@hiqua)
### Fixed ### Fixed
- Fix small dialog buttons (@kalina559, #1096) - Fix small dialog buttons (@kalina559, #1096)
- Fix invalid CSV files (@hiqua, #1177) - Fix invalid CSV files (@hiqua, #1177)
- Fix small issues in calendar chart (@kalina559, #1314) - Fix small issues in calendar chart (@kalina559, #1314)
@ -84,13 +116,16 @@
- Fix widgets not working correctly on API 33 (@iSoron, #1488) - Fix widgets not working correctly on API 33 (@iSoron, #1488)
### Refactoring & Testing ### Refactoring & Testing
- Replace raster icons by vector assets (@kalina559) - Replace raster icons by vector assets (@kalina559)
- Remove JVM dependencies from uhabits-core module (@sgallese) - Remove JVM dependencies from uhabits-core module (@sgallese)
- Add various missing tests (@sgallese) - Add various missing tests (@sgallese)
- Upgrade project dependencies (@hiqua, @sgallese) - Upgrade project dependencies (@hiqua, @sgallese)
## [2.0.3] - 2021-08-21 ## [2.0.3] - 2021-08-21
### Fixed ### Fixed
- Improve automatic checkmarks for monthly habits (@iSoron, #947) - Improve automatic checkmarks for monthly habits (@iSoron, #947)
- Fix small theme issues (@iSoron) - Fix small theme issues (@iSoron)
- Fix ANR on some Samsung phones (@iSoron, #962) - Fix ANR on some Samsung phones (@iSoron, #962)
@ -102,9 +137,11 @@
## [2.0.2] - 2021-05-23 ## [2.0.2] - 2021-05-23
### Changed ### Changed
- Make checkmark widget resizable - Make checkmark widget resizable
### Fixed ### Fixed
- Fix crash caused by numerical habits with zero target (@iSoron, #903) - Fix crash caused by numerical habits with zero target (@iSoron, #903)
- Fix small issues with font size (@iSoron) - Fix small issues with font size (@iSoron)
- Allow fractional target values (@sumanabhi, #911) - Allow fractional target values (@sumanabhi, #911)
@ -115,18 +152,22 @@
## [2.0.1] - 2021-05-09 ## [2.0.1] - 2021-05-09
### Added ### Added
- Make midnight delay optional and disabled by default (@hiqua) - Make midnight delay optional and disabled by default (@hiqua)
- Add arrows to sort menu (@iSoron) - Add arrows to sort menu (@iSoron)
### Removed ### Removed
- Temporarily remove experimental device sync functionality. This feature will be re-added in - Temporarily remove experimental device sync functionality. This feature will be re-added in
Loop 2.1. Loop 2.1.
### Changed ### Changed
- Make implicit checkmarks easier to read (@iSoron) - Make implicit checkmarks easier to read (@iSoron)
- Update and improve list of translators (@hiqua, @iSoron) - Update and improve list of translators (@hiqua, @iSoron)
### Fixed ### Fixed
- Disable transparency for stacked widgets (@hiqua) - Disable transparency for stacked widgets (@hiqua)
- Fix various color issues on the dark theme (@hiqua, @iSoron) - Fix various color issues on the dark theme (@hiqua, @iSoron)
- Fix "customize notifications" on older devices (@hiqua) - Fix "customize notifications" on older devices (@hiqua)
@ -135,6 +176,7 @@
- Fix checkmark widget not rendering properly on some Samsung phones (@iSoron) - Fix checkmark widget not rendering properly on some Samsung phones (@iSoron)
### Refactoring & Testing ### Refactoring & Testing
- Finish conversion of the entire project to Kotlin (@hiqua, @iSoron, @MarKco) - Finish conversion of the entire project to Kotlin (@hiqua, @iSoron, @MarKco)
- Automatically run large tests on GitHub Actions (@iSoron) - Automatically run large tests on GitHub Actions (@iSoron)
- Remove unused v21 resources (@hiqua) - Remove unused v21 resources (@hiqua)
@ -142,6 +184,7 @@
## [2.0.0-alpha] - 2020-11-29 ## [2.0.0-alpha] - 2020-11-29
### Added ### Added
- Track numeric habits (@iSoron, @namnl) - Track numeric habits (@iSoron, @namnl)
- Skip days without breaking streak (@KristianTashkov) - Skip days without breaking streak (@KristianTashkov)
- Sort habits by status (@hiqua) - Sort habits by status (@hiqua)
@ -152,15 +195,18 @@
- Export backups daily (@iSoron) - Export backups daily (@iSoron)
### Removed ### Removed
- Drop support to devices older than Android 6.0 (API 23) - Drop support to devices older than Android 6.0 (API 23)
### Fixed ### Fixed
- Reset chart offset when switching scale (@alxmjo) - Reset chart offset when switching scale (@alxmjo)
- Don't show reminders from archived habits (@KristianTashkov) - Don't show reminders from archived habits (@KristianTashkov)
- Lapses on non-daily habits decrease the score too much (@iSoron) - Lapses on non-daily habits decrease the score too much (@iSoron)
- Update widgets at midnight (@KristianTashkov) - Update widgets at midnight (@KristianTashkov)
### Refactoring ### Refactoring
- Convert files to Kotlin (@olegivo) - Convert files to Kotlin (@olegivo)
## [1.8.12] - 2021-01-30 ## [1.8.12] - 2021-01-30
@ -185,13 +231,15 @@
## [1.8.8] - 2020-06-21 ## [1.8.8] - 2020-06-21
- Make small changes to the habit scheduling algorithm, so that "1 time every x days" habits work more predictably. - Make small changes to the habit scheduling algorithm, so that "1 time every x days" habits work
more predictably.
- Fix crash when saving habit - Fix crash when saving habit
## [1.8.0] - 2020-01-01 ## [1.8.0] - 2020-01-01
- New bar chart showing number of repetitions performed in each week, month, quarter or year. - New bar chart showing number of repetitions performed in each week, month, quarter or year.
- Improved calculation of streaks for non-daily habits: performing habits on irregular weekdays will no longer break your streak. - Improved calculation of streaks for non-daily habits: performing habits on irregular weekdays will
no longer break your streak.
- Many more colors to choose from (now 20 in total). - Many more colors to choose from (now 20 in total).
- Ability to customize how transparent the widgets are on your home screen. - Ability to customize how transparent the widgets are on your home screen.
- Ability to customize the first day of the week. - Ability to customize the first day of the week.

@ -36,7 +36,7 @@ Updating gradle might fix this, so try again in the future to remove this and ru
If this doesn't produce any warning, try to remove it. If this doesn't produce any warning, try to remove it.
*/ */
kotlin { kotlin {
jvmToolchain(11) jvmToolchain(17)
} }
android { android {
@ -44,8 +44,8 @@ android {
compileSdk = 36 compileSdk = 36
defaultConfig { defaultConfig {
versionCode = 20300 versionCode = 20301
versionName = "2.3.0" versionName = "2.3.1"
minSdk = 28 minSdk = 28
targetSdk = 36 targetSdk = 36
applicationId = "org.isoron.uhabits" applicationId = "org.isoron.uhabits"
@ -79,11 +79,11 @@ android {
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
targetCompatibility(JavaVersion.VERSION_11) targetCompatibility(JavaVersion.VERSION_17)
sourceCompatibility(JavaVersion.VERSION_11) sourceCompatibility(JavaVersion.VERSION_17)
} }
kotlinOptions.jvmTarget = JavaVersion.VERSION_11.toString() kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString()
buildFeatures.viewBinding = true buildFeatures.viewBinding = true
lint.abortOnError = false lint.abortOnError = false
} }

@ -22,7 +22,7 @@ package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.junit.Before import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -35,23 +35,22 @@ class EmptyListViewTest : BaseViewTest() {
} }
private val path = "habits/list/EmptyListView" private val path = "habits/list/EmptyListView"
private val view: EmptyListView = EmptyListView(targetContext)
@Before
override fun setUp() {
super.setUp()
measureView(view, dpToPixels(200), dpToPixels(200))
}
@Test @Test
@Ignore("non-deterministic failure")
fun testRender_done() { fun testRender_done() {
val view = EmptyListView(targetContext)
view.showDone() view.showDone()
measureView(view, dpToPixels(200), dpToPixels(200))
assertRenders(view, "$path/done.png") assertRenders(view, "$path/done.png")
} }
@Test @Test
@Ignore("non-deterministic failure")
fun testRender_empty() { fun testRender_empty() {
val view = EmptyListView(targetContext)
view.showEmpty() view.showEmpty()
measureView(view, dpToPixels(200), dpToPixels(200))
assertRenders(view, "$path/empty.png") assertRenders(view, "$path/empty.png")
} }
} }

@ -25,6 +25,7 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.io.IOException import java.io.IOException
@ -57,6 +58,7 @@ class CheckmarkWidgetViewTest : BaseViewTest() {
@Test @Test
@Throws(IOException::class) @Throws(IOException::class)
@Ignore("non-deterministic")
fun testRender_checked() { fun testRender_checked() {
assertRenders(view, PATH + "checked.png") assertRenders(view, PATH + "checked.png")
} }

@ -26,6 +26,7 @@ import org.isoron.uhabits.BuildConfig
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.databinding.AboutBinding import org.isoron.uhabits.databinding.AboutBinding
import org.isoron.uhabits.utils.applyBottomInset
import org.isoron.uhabits.utils.applyRootViewInsets import org.isoron.uhabits.utils.applyRootViewInsets
import org.isoron.uhabits.utils.currentTheme import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.setupToolbar import org.isoron.uhabits.utils.setupToolbar
@ -55,6 +56,7 @@ class AboutView(
binding.tvTranslate.setOnClickListener { screen.showTranslationWebsite() } binding.tvTranslate.setOnClickListener { screen.showTranslationWebsite() }
binding.tvVersion.setOnClickListener { screen.onPressDeveloperCountdown() } binding.tvVersion.setOnClickListener { screen.onPressDeveloperCountdown() }
binding.tvVersion.text = String.format(version, BuildConfig.VERSION_NAME) binding.tvVersion.text = String.format(version, BuildConfig.VERSION_NAME)
binding.outerLinearLayout.applyBottomInset()
applyRootViewInsets() applyRootViewInsets()
} }
} }

@ -204,7 +204,7 @@ class RingView : View {
val res = StyledResources(context) val res = StyledResources(context)
if (backgroundColor == null) backgroundColor = res.getColor(R.attr.cardBgColor) if (backgroundColor == null) backgroundColor = res.getColor(R.attr.cardBgColor)
if (inactiveColor == null) inactiveColor = res.getColor(R.attr.contrast100) if (inactiveColor == null) inactiveColor = res.getColor(R.attr.contrast100)
inactiveColor = setAlpha(inactiveColor!!, 0.1f) inactiveColor = setAlpha(inactiveColor!!, 0.15f)
rect = RectF() rect = RectF()
} }

@ -23,6 +23,7 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy import dagger.Lazy
import nl.dionsegijn.konfetti.core.Party import nl.dionsegijn.konfetti.core.Party
@ -226,6 +227,14 @@ class ListHabitsScreen
override fun showConfetti(color: PaletteColor, x: Float, y: Float) { override fun showConfetti(color: PaletteColor, x: Float, y: Float) {
if (x == 0f && y == 0f) return if (x == 0f && y == 0f) return
if (preferences.isConfettiAnimationDisabled) return if (preferences.isConfettiAnimationDisabled) return
if (Settings.Global.getFloat(
activity.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f
) == 0f
) {
return
}
val baseColor = themeSwitcher.currentTheme!!.color(color).toInt() val baseColor = themeSwitcher.currentTheme!!.color(color).toInt()
rootView.get().konfettiView.start( rootView.get().konfettiView.start(
Party( Party(
@ -299,30 +308,36 @@ class ListHabitsScreen
command.selected.size command.selected.size
) )
} }
is ChangeHabitColorCommand -> { is ChangeHabitColorCommand -> {
return activity.resources.getQuantityString( return activity.resources.getQuantityString(
R.plurals.toast_habits_changed, R.plurals.toast_habits_changed,
command.selected.size command.selected.size
) )
} }
is CreateHabitCommand -> { is CreateHabitCommand -> {
return activity.resources.getString(R.string.toast_habit_created) return activity.resources.getString(R.string.toast_habit_created)
} }
is DeleteHabitsCommand -> { is DeleteHabitsCommand -> {
return activity.resources.getQuantityString( return activity.resources.getQuantityString(
R.plurals.toast_habits_deleted, R.plurals.toast_habits_deleted,
command.selected.size command.selected.size
) )
} }
is EditHabitCommand -> { is EditHabitCommand -> {
return activity.resources.getQuantityString(R.plurals.toast_habits_changed, 1) return activity.resources.getQuantityString(R.plurals.toast_habits_changed, 1)
} }
is UnarchiveHabitsCommand -> { is UnarchiveHabitsCommand -> {
return activity.resources.getQuantityString( return activity.resources.getQuantityString(
R.plurals.toast_habits_unarchived, R.plurals.toast_habits_unarchived,
command.selected.size command.selected.size
) )
} }
else -> return null else -> return null
} }
} }
@ -335,9 +350,11 @@ class ListHabitsScreen
adapter.refresh() adapter.refresh()
activity.showMessage(activity.resources.getString(R.string.habits_imported)) activity.showMessage(activity.resources.getString(R.string.habits_imported))
} }
ImportDataTask.NOT_RECOGNIZED -> { ImportDataTask.NOT_RECOGNIZED -> {
activity.showMessage(activity.resources.getString(R.string.file_not_recognized)) activity.showMessage(activity.resources.getString(R.string.file_not_recognized))
} }
else -> { else -> {
activity.showMessage(activity.resources.getString(R.string.could_not_import)) activity.showMessage(activity.resources.getString(R.string.could_not_import))
} }

@ -20,11 +20,14 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.Context import android.content.Context
import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.GestureDetector import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.END import androidx.recyclerview.widget.ItemTouchHelper.END
@ -57,6 +60,7 @@ class HabitCardListView(
) : RecyclerView(context, null, R.attr.scrollableRecyclerViewStyle) { ) : RecyclerView(context, null, R.attr.scrollableRecyclerViewStyle) {
var checkmarkCount: Int = 0 var checkmarkCount: Int = 0
private var insetDecorationsAdded: Boolean = false
var dataOffset: Int = 0 var dataOffset: Int = 0
set(value) { set(value) {
@ -75,9 +79,32 @@ class HabitCardListView(
setHasFixedSize(true) setHasFixedSize(true)
isLongClickable = true isLongClickable = true
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
applyBottomInset()
super.setAdapter(adapter) super.setAdapter(adapter)
} }
private fun applyBottomInset() {
ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets ->
if (insetDecorationsAdded) return@setOnApplyWindowInsetsListener insets
insetDecorationsAdded = true
val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
addItemDecoration(object : ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: State
) {
val itemCount = parent.adapter?.itemCount
if (parent.getChildAdapterPosition(view) == itemCount?.minus(1)) {
outRect.bottom = systemBarsInsets.bottom
}
}
})
insets
}
}
fun createHabitCardView(): HabitCardView { fun createHabitCardView(): HabitCardView {
return cardViewFactory.create() return cardViewFactory.create()
} }

@ -238,17 +238,18 @@ class HabitCardView(
private fun getAbsoluteButtonLocation(timestamp: Timestamp): PointF { private fun getAbsoluteButtonLocation(timestamp: Timestamp): PointF {
val containerLocation = IntArray(2) val containerLocation = IntArray(2)
this.getLocationOnScreen(containerLocation) this.getLocationInWindow(containerLocation)
val relButtonLocation = getRelativeButtonLocation(timestamp) val relButtonLocation = getRelativeButtonLocation(timestamp)
val windowInsets = rootWindowInsets val windowInsets = rootWindowInsets
val statusBarHeight = if (SDK_INT <= Build.VERSION_CODES.VANILLA_ICE_CREAM) { val xInset = windowInsets?.displayCutout?.safeInsetLeft ?: 0
val yInset = if (SDK_INT <= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
windowInsets?.systemWindowInsetTop ?: 0 windowInsets?.systemWindowInsetTop ?: 0
} else { } else {
0 0
} }
return PointF( return PointF(
containerLocation[0].toFloat() + relButtonLocation.x, containerLocation[0].toFloat() + relButtonLocation.x - xInset,
containerLocation[1].toFloat() + relButtonLocation.y - statusBarHeight containerLocation[1].toFloat() + relButtonLocation.y - yInset
) )
} }

@ -189,16 +189,19 @@ class NumberButtonView(
textSize = dim(R.dimen.smallTextSize) textSize = dim(R.dimen.smallTextSize)
typeface = getFontAwesome() typeface = getFontAwesome()
} }
value >= 0 -> { value >= 0 -> {
label = value.toShortString() label = value.toShortString()
typeface = BOLD_TYPEFACE typeface = BOLD_TYPEFACE
textSize = dim(R.dimen.smallTextSize) textSize = dim(R.dimen.smallTextSize)
} }
preferences.areQuestionMarksEnabled -> { preferences.areQuestionMarksEnabled -> {
label = resources.getString(R.string.fa_question) label = resources.getString(R.string.fa_question)
typeface = getFontAwesome() typeface = getFontAwesome()
textSize = dim(R.dimen.smallerTextSize) textSize = dim(R.dimen.smallerTextSize)
} }
else -> { else -> {
label = "0" label = "0"
typeface = BOLD_TYPEFACE typeface = BOLD_TYPEFACE
@ -212,14 +215,23 @@ class NumberButtonView(
pUnit.color = activeColor pUnit.color = activeColor
if (units.isBlank()) { if (units.isBlank()) {
// Draw number without units
rect.set(0f, 0f, width.toFloat(), height.toFloat()) rect.set(0f, 0f, width.toFloat(), height.toFloat())
rect.offset(0f, 0.5f * em) rect.offset(0f, 0.5f * em)
canvas.drawText(label, rect.centerX(), rect.centerY(), pNumber) canvas.drawText(label, rect.centerX(), rect.centerY(), pNumber)
} else { } else {
// Draw number
rect.set(0f, 0f, width.toFloat(), height.toFloat()) rect.set(0f, 0f, width.toFloat(), height.toFloat())
canvas.drawText(label, rect.centerX(), rect.centerY(), pNumber) canvas.drawText(label, rect.centerX(), rect.centerY(), pNumber)
// Draw units
val maxUnitsWidth = width * 0.9f
var trimmedUnits = units
while (trimmedUnits.length > 2 && pUnit.measureText(trimmedUnits) > maxUnitsWidth) {
trimmedUnits = trimmedUnits.dropLast(2) + ""
}
rect.offset(0f, 1.3f * em) rect.offset(0f, 1.3f * em)
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit) canvas.drawText(trimmedUnits, rect.centerX(), rect.centerY(), pUnit)
} }
drawNotesIndicator(canvas, color, em, notes) drawNotesIndicator(canvas, color, em, notes)

@ -25,6 +25,7 @@ import android.widget.FrameLayout
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitState import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitState
import org.isoron.uhabits.databinding.ShowHabitBinding import org.isoron.uhabits.databinding.ShowHabitBinding
import org.isoron.uhabits.utils.applyBottomInset
import org.isoron.uhabits.utils.applyToolbarInsets import org.isoron.uhabits.utils.applyToolbarInsets
import org.isoron.uhabits.utils.setupToolbar import org.isoron.uhabits.utils.setupToolbar
@ -57,6 +58,7 @@ class ShowHabitView(context: Context) : FrameLayout(context) {
} else { } else {
binding.targetCard.visibility = GONE binding.targetCard.visibility = GONE
} }
binding.linearLayout.applyBottomInset()
} }
fun setListener(presenter: ShowHabitPresenter) { fun setListener(presenter: ShowHabitPresenter) {

@ -26,6 +26,7 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.databinding.SettingsActivityBinding import org.isoron.uhabits.databinding.SettingsActivityBinding
import org.isoron.uhabits.utils.applyBottomInset
import org.isoron.uhabits.utils.applyRootViewInsets import org.isoron.uhabits.utils.applyRootViewInsets
import org.isoron.uhabits.utils.setupToolbar import org.isoron.uhabits.utils.setupToolbar
@ -44,6 +45,7 @@ class SettingsActivity : AppCompatActivity() {
theme = themeSwitcher.currentTheme theme = themeSwitcher.currentTheme
) )
binding.root.applyRootViewInsets() binding.root.applyRootViewInsets()
binding.root.applyBottomInset()
setContentView(binding.root) setContentView(binding.root)
} }
} }

@ -260,6 +260,14 @@ fun View.applyRootViewInsets() {
} }
} }
fun View.applyBottomInset() {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(0, 0, 0, systemBarsInsets.bottom)
insets
}
}
fun View.applyToolbarInsets() { fun View.applyToolbarInsets() {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets -> ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
<!--
~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org> ~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
~ ~
~ This file is part of Loop Habit Tracker. ~ This file is part of Loop Habit Tracker.
@ -19,16 +18,16 @@
--> -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?windowBackgroundColor" android:background="?windowBackgroundColor"
android:fillViewport="true"> android:fillViewport="true">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
app:popupTheme="?toolbarPopupTheme" app:popupTheme="?toolbarPopupTheme"
style="@style/Toolbar"/> style="@style/Toolbar" />
<ScrollView <ScrollView
android:id="@+id/scrollView" android:id="@+id/scrollView"
@ -37,6 +36,7 @@
android:layout_below="@id/toolbar"> android:layout_below="@id/toolbar">
<LinearLayout <LinearLayout
android:id="@+id/outerLinearLayout"
style="@style/CardList"> style="@style/CardList">
<LinearLayout <LinearLayout
@ -47,7 +47,7 @@
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"
android:layout_margin="6dp" android:layout_margin="6dp"
android:src="@drawable/intro_icon_1"/> android:src="@drawable/intro_icon_1" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -57,14 +57,14 @@
android:textSize="16sp" android:textSize="16sp"
android:layout_margin="6dp" android:layout_margin="6dp"
android:textColor="?aboutScreenColor" android:textColor="?aboutScreenColor"
android:text="@string/app_name"/> android:text="@string/app_name" />
<TextView <TextView
android:id="@+id/tvVersion" android:id="@+id/tvVersion"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:text=""/> android:text="" />
</LinearLayout> </LinearLayout>
@ -75,32 +75,32 @@
<TextView <TextView
style="@style/CardHeader" style="@style/CardHeader"
android:text="@string/links" android:text="@string/links"
android:textColor="?aboutScreenColor"/> android:textColor="?aboutScreenColor" />
<TextView <TextView
android:id="@+id/tvRate" android:id="@+id/tvRate"
style="@style/About.Item.Clickable" style="@style/About.Item.Clickable"
android:text="@string/pref_rate_this_app"/> android:text="@string/pref_rate_this_app" />
<TextView <TextView
android:id="@+id/tvFeedback" android:id="@+id/tvFeedback"
style="@style/About.Item.Clickable" style="@style/About.Item.Clickable"
android:text="@string/pref_send_feedback"/> android:text="@string/pref_send_feedback" />
<TextView <TextView
android:id="@+id/tvTranslate" android:id="@+id/tvTranslate"
style="@style/About.Item.Clickable" style="@style/About.Item.Clickable"
android:text="@string/help_translate"/> android:text="@string/help_translate" />
<TextView <TextView
android:id="@+id/tvSource" android:id="@+id/tvSource"
style="@style/About.Item.Clickable" style="@style/About.Item.Clickable"
android:text="@string/pref_view_source_code"/> android:text="@string/pref_view_source_code" />
<TextView <TextView
android:id="@+id/tvPrivacy" android:id="@+id/tvPrivacy"
style="@style/About.Item.Clickable" style="@style/About.Item.Clickable"
android:text="@string/pref_view_privacy"/> android:text="@string/pref_view_privacy" />
</LinearLayout> </LinearLayout>
@ -111,7 +111,7 @@
<TextView <TextView
style="@style/CardHeader" style="@style/CardHeader"
android:text="@string/developers" android:text="@string/developers"
android:textColor="?aboutScreenColor"/> android:textColor="?aboutScreenColor" />
<!-- <!--
This file lists developers who contributed with at least 100 lines This file lists developers who contributed with at least 100 lines
@ -120,31 +120,76 @@
linked at the bottom of the list. If you qualify, please feel free to linked at the bottom of the list. If you qualify, please feel free to
submit a pull request adding yourself here. submit a pull request adding yourself here.
--> -->
<TextView style="@style/About.Item" android:text="Álinson S. Xavier (@iSoron)"/> <TextView
<TextView style="@style/About.Item" android:text="Quentin Hibon (@hiqua)"/> style="@style/About.Item"
<TextView style="@style/About.Item" android:text="Oleg Ivashchenko (@olegivo)"/> android:text="Álinson S. Xavier (@iSoron)" />
<TextView style="@style/About.Item" android:text="Kristian Tashkov (@KristianTashkov)"/>
<TextView style="@style/About.Item" android:text="Jakub Kalinowski (@kalina559)"/> <TextView
<TextView style="@style/About.Item" android:text="Rechee Jozil (@recheej)"/> style="@style/About.Item"
<TextView style="@style/About.Item" android:text="Sebastian Gallese (@sgallese)"/> android:text="Quentin Hibon (@hiqua)" />
<TextView style="@style/About.Item" android:text="Luboš Luňák (@llunak)"/>
<TextView style="@style/About.Item" android:text="Bindu (@vbh)"/> <TextView
<TextView style="@style/About.Item" android:text="Victor Yu (@vyu1)"/> style="@style/About.Item"
<TextView style="@style/About.Item" android:text="Christoph Hennemann (@chennemann)"/> android:text="Oleg Ivashchenko (@olegivo)" />
<TextView style="@style/About.Item" android:text="Денис (@sciamano)"/>
<TextView style="@style/About.Item" android:text="Joseph Tran (@JotraN)"/> <TextView
<TextView style="@style/About.Item" android:text="Nikhil (@regularcoder)"/> style="@style/About.Item"
<TextView style="@style/About.Item" android:text="JanetQC"/> android:text="Kristian Tashkov (@KristianTashkov)" />
<TextView
android:id="@+id/tvContributors" <TextView
style="@style/About.Item.Clickable" style="@style/About.Item"
android:text="@string/view_all_contributors"/> android:text="Jakub Kalinowski (@kalina559)" />
<TextView
style="@style/About.Item"
android:text="Rechee Jozil (@recheej)" />
<TextView
style="@style/About.Item"
android:text="Sebastian Gallese (@sgallese)" />
<TextView
style="@style/About.Item"
android:text="Luboš Luňák (@llunak)" />
<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"
style="@style/About.Item.Clickable"
android:text="@string/view_all_contributors" />
</LinearLayout> </LinearLayout>
<include layout="@layout/about_translators"/> <include layout="@layout/about_translators" />
> >
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

@ -17,8 +17,7 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>. ~ with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<RelativeLayout <RelativeLayout android:id="@+id/container"
android:id="@+id/container"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -29,7 +28,7 @@
android:id="@+id/toolbar" android:id="@+id/toolbar"
style="@style/Toolbar" style="@style/Toolbar"
app:popupTheme="?toolbarPopupTheme" app:popupTheme="?toolbarPopupTheme"
android:layout_alignParentTop="true"/> android:layout_alignParentTop="true" />
<ScrollView <ScrollView
android:id="@+id/scrollView" android:id="@+id/scrollView"
@ -41,11 +40,12 @@
<LinearLayout <LinearLayout
style="@style/CardList" style="@style/CardList"
android:id="@+id/linearLayout"
android:clipToPadding="false"> android:clipToPadding="false">
<org.isoron.uhabits.activities.habits.show.views.SubtitleCardView <org.isoron.uhabits.activities.habits.show.views.SubtitleCardView
android:id="@+id/subtitleCard" android:id="@+id/subtitleCard"
style="@style/ShowHabit.Subtitle"/> style="@style/ShowHabit.Subtitle" />
<org.isoron.uhabits.activities.habits.show.views.NotesCardView <org.isoron.uhabits.activities.habits.show.views.NotesCardView
android:id="@+id/notesCard" android:id="@+id/notesCard"
@ -55,36 +55,36 @@
<org.isoron.uhabits.activities.habits.show.views.OverviewCardView <org.isoron.uhabits.activities.habits.show.views.OverviewCardView
android:id="@+id/overviewCard" android:id="@+id/overviewCard"
style="@style/Card" style="@style/Card"
android:paddingTop="12dp"/> android:paddingTop="12dp" />
<org.isoron.uhabits.activities.habits.show.views.TargetCardView <org.isoron.uhabits.activities.habits.show.views.TargetCardView
android:id="@+id/targetCard" android:id="@+id/targetCard"
style="@style/Card" style="@style/Card"
android:paddingTop="12dp"/> android:paddingTop="12dp" />
<org.isoron.uhabits.activities.habits.show.views.ScoreCardView <org.isoron.uhabits.activities.habits.show.views.ScoreCardView
android:id="@+id/scoreCard" android:id="@+id/scoreCard"
style="@style/Card" style="@style/Card"
android:gravity="center"/> android:gravity="center" />
<org.isoron.uhabits.activities.habits.show.views.BarCardView <org.isoron.uhabits.activities.habits.show.views.BarCardView
android:id="@+id/barCard" android:id="@+id/barCard"
style="@style/Card" style="@style/Card"
android:gravity="center"/> android:gravity="center" />
<org.isoron.uhabits.activities.habits.show.views.HistoryCardView <org.isoron.uhabits.activities.habits.show.views.HistoryCardView
android:id="@+id/historyCard" android:id="@+id/historyCard"
style="@style/Card" style="@style/Card"
android:gravity="center" android:gravity="center"
android:paddingBottom="0dp"/> android:paddingBottom="0dp" />
<org.isoron.uhabits.activities.habits.show.views.StreakCardView <org.isoron.uhabits.activities.habits.show.views.StreakCardView
android:id="@+id/streakCard" android:id="@+id/streakCard"
style="@style/Card"/> style="@style/Card" />
<org.isoron.uhabits.activities.habits.show.views.FrequencyCardView <org.isoron.uhabits.activities.habits.show.views.FrequencyCardView
android:id="@+id/frequencyCard" android:id="@+id/frequencyCard"
style="@style/Card"/> style="@style/Card" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
<!--
~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org> ~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
~ ~
~ This file is part of Loop Habit Tracker. ~ This file is part of Loop Habit Tracker.
@ -32,8 +31,7 @@
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:textColor="?attr/contrast60" android:textColor="?attr/contrast60"
android:textSize="@dimen/regularTextSize" android:textSize="@dimen/regularTextSize"
tools:text="Have you worked out today?" tools:text="Have you worked out today?" />
/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -54,12 +52,14 @@
android:id="@+id/targetText" android:id="@+id/targetText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="8.5k" android:text=""
android:textColor="?attr/contrast60" android:textColor="?attr/contrast60"
android:textSize="@dimen/smallTextSize" android:textSize="@dimen/smallTextSize"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
/> android:maxEms="7"
android:maxLines="1"
android:ellipsize="end" />
<TextView <TextView
android:id="@+id/frequencyIcon" android:id="@+id/frequencyIcon"
@ -93,7 +93,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingTop="1dp" android:paddingTop="1dp"
android:textColor="?attr/contrast60" android:textColor="?attr/contrast60"
android:text="8:00 AM" android:text=""
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:textSize="@dimen/smallTextSize" /> android:textSize="@dimen/smallTextSize" />

@ -126,8 +126,9 @@
<item name="highlightedBackgroundColor">@color/black</item> <item name="highlightedBackgroundColor">@color/black</item>
<item name="contrast0">@color/black</item> <item name="contrast0">@color/black</item>
<item name="contrast20">@color/grey_900</item> <item name="contrast20">@color/grey_900</item>
<item name="contrast60">@color/grey_700</item> <item name="contrast40">@color/grey_800</item>
<item name="contrast80">@color/grey_700</item> <item name="contrast60">@color/grey_500</item>
<item name="contrast80">@color/grey_400</item>
<item name="contrast100">@color/grey_200</item> <item name="contrast100">@color/grey_200</item>
<item name="selectedBackground">@drawable/selected_box</item> <item name="selectedBackground">@drawable/selected_box</item>
<item name="textColorAlertDialogListItem">@color/grey_100</item> <item name="textColorAlertDialogListItem">@color/grey_100</item>

@ -0,0 +1 @@
Date,Value,Notes
1 Date Value Notes
1 Date Value Notes

@ -1 +1,2 @@
Date,Score
2015-01-25,0.0000 2015-01-25,0.0000

1 2015-01-25 Date 0.0000 Score
1 Date Score
2 2015-01-25 2015-01-25 0.0000 0.0000

@ -1,10 +1,11 @@
2015-01-25,2 Date,Value,Notes
2015-01-24,0 2015-01-25,YES_MANUAL,
2015-01-23,1 2015-01-24,NO,Sick
2015-01-22,2 2015-01-23,YES_AUTO,"Forgot to do it, really"
2015-01-21,2 2015-01-22,YES_MANUAL,
2015-01-20,2 2015-01-21,YES_MANUAL,
2015-01-19,1 2015-01-20,YES_MANUAL,
2015-01-18,1 2015-01-19,YES_AUTO,"""Vacation"""
2015-01-17,2 2015-01-18,YES_AUTO,
2015-01-16,2 2015-01-17,YES_MANUAL,
2015-01-16,YES_MANUAL,

1 2015-01-25 Date 2 Value Notes
2 2015-01-24 2015-01-25 0 YES_MANUAL
3 2015-01-23 2015-01-24 1 NO Sick
4 2015-01-22 2015-01-23 2 YES_AUTO Forgot to do it, really
5 2015-01-21 2015-01-22 2 YES_MANUAL
6 2015-01-20 2015-01-21 2 YES_MANUAL
7 2015-01-19 2015-01-20 1 YES_MANUAL
8 2015-01-18 2015-01-19 1 YES_AUTO "Vacation"
9 2015-01-17 2015-01-18 2 YES_AUTO
10 2015-01-16 2015-01-17 2 YES_MANUAL
11 2015-01-16 YES_MANUAL

@ -1,3 +1,4 @@
Date,Score
2015-01-25,0.2557 2015-01-25,0.2557
2015-01-24,0.2226 2015-01-24,0.2226
2015-01-23,0.1991 2015-01-23,0.1991

1 2015-01-25 Date 0.2557 Score
1 Date Score
2 2015-01-25 2015-01-25 0.2557 0.2557
3 2015-01-24 2015-01-24 0.2226 0.2226
4 2015-01-23 2015-01-23 0.1991 0.1991

@ -1,11 +1,11 @@
Date,Meditate,Wake up early, Date,Meditate,Wake up early,
2015-01-25,-1,2, 2015-01-25,UNKNOWN,YES_MANUAL,
2015-01-24,-1,0, 2015-01-24,UNKNOWN,NO,
2015-01-23,-1,1, 2015-01-23,UNKNOWN,YES_AUTO,
2015-01-22,-1,2, 2015-01-22,UNKNOWN,YES_MANUAL,
2015-01-21,-1,2, 2015-01-21,UNKNOWN,YES_MANUAL,
2015-01-20,-1,2, 2015-01-20,UNKNOWN,YES_MANUAL,
2015-01-19,-1,1, 2015-01-19,UNKNOWN,YES_AUTO,
2015-01-18,-1,1, 2015-01-18,UNKNOWN,YES_AUTO,
2015-01-17,-1,2, 2015-01-17,UNKNOWN,YES_MANUAL,
2015-01-16,-1,2, 2015-01-16,UNKNOWN,YES_MANUAL,

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

@ -1,3 +1,3 @@
Position,Name,Question,Description,NumRepetitions,Interval,Color Position,Name,Type,Question,Description,FrequencyNumerator,FrequencyDenominator,Color,Unit,Target Type,Target Value,Archived?
001,Meditate,Did you meditate this morning?,,1,1,#FF8F00 001,Meditate,YES_NO,Did you meditate this morning?,,1,1,#FF8F00,,,,false
002,Wake up early,Did you wake up before 6am?,,2,3,#00897B 002,Wake up early,YES_NO,Did you wake up before 6am?,,2,3,#00897B,,,,false

1 Position Name Type Question NumRepetitions Description Interval FrequencyNumerator FrequencyDenominator Color Unit Target Type Target Value Archived?
2 001 Meditate YES_NO Did you meditate this morning? 1 1 1 1 #FF8F00 false
3 002 Wake up early YES_NO Did you wake up before 6am? 2 3 2 3 #00897B false

@ -24,7 +24,7 @@ plugins {
kotlin { kotlin {
jvm().withJava() jvm().withJava()
jvmToolchain(11) jvmToolchain(17)
sourceSets { sourceSets {
val commonMain by getting { val commonMain by getting {

@ -18,6 +18,7 @@
*/ */
package org.isoron.uhabits.core.io package org.isoron.uhabits.core.io
import com.opencsv.CSVWriter
import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.EntryList import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
@ -32,7 +33,6 @@ import java.io.FileOutputStream
import java.io.FileWriter import java.io.FileWriter
import java.io.IOException import java.io.IOException
import java.io.Writer import java.io.Writer
import java.util.ArrayList
import java.util.LinkedList import java.util.LinkedList
import java.util.Locale import java.util.Locale
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@ -109,11 +109,14 @@ class HabitsCSVExporter(
var oldest = today var oldest = today
val known = habit.computedEntries.getKnown() val known = habit.computedEntries.getKnown()
if (known.isNotEmpty()) oldest = known[known.size - 1].timestamp if (known.isNotEmpty()) oldest = known[known.size - 1].timestamp
val csv = CSVWriter(out)
csv.writeNext(arrayOf("Date", "Score"), false)
for ((timestamp1, value) in habit.scores.getByInterval(oldest, today)) { for ((timestamp1, value) in habit.scores.getByInterval(oldest, today)) {
val timestamp = dateFormat.format(timestamp1.unixTime) val timestamp = dateFormat.format(timestamp1.unixTime)
val score = String.format(Locale.US, "%.4f", value) val score = String.format(Locale.US, "%.4f", value)
out.write(String.format("%s,%s\n", timestamp, score)) csv.writeNext(arrayOf(timestamp, score), false)
} }
csv.close()
out.close() out.close()
} }
@ -122,10 +125,20 @@ class HabitsCSVExporter(
val out = FileWriter(exportDirName + filename) val out = FileWriter(exportDirName + filename)
generatedFilenames.add(filename) generatedFilenames.add(filename)
val dateFormat = DateFormats.getCSVDateFormat() val dateFormat = DateFormats.getCSVDateFormat()
for ((timestamp, value) in entries.getKnown()) { val csv = CSVWriter(out)
val date = dateFormat.format(timestamp.toJavaDate()) csv.writeNext(arrayOf("Date", "Value", "Notes"), false)
out.write(String.format(Locale.US, "%s,%d\n", date, value)) for (entry in entries.getKnown()) {
val date = dateFormat.format(entry.timestamp.toJavaDate())
csv.writeNext(
arrayOf(
date,
entry.formattedValue,
entry.notes
),
false
)
} }
csv.close()
out.close() out.close()
} }
@ -167,7 +180,7 @@ class HabitsCSVExporter(
checksWriter.write(sb.toString()) checksWriter.write(sb.toString())
scoresWriter.write(sb.toString()) scoresWriter.write(sb.toString())
for (j in selectedHabits.indices) { for (j in selectedHabits.indices) {
checksWriter.write(checkmarks[j][i].value.toString()) checksWriter.write(checkmarks[j][i].formattedValue)
checksWriter.write(delimiter) checksWriter.write(delimiter)
val score = String.format(Locale.US, "%.4f", scores[j][i].value) val score = String.format(Locale.US, "%.4f", scores[j][i].value)
scoresWriter.write(score) scoresWriter.write(score)

@ -23,6 +23,16 @@ data class Entry(
val value: Int, val value: Int,
val notes: String = "" val notes: String = ""
) { ) {
val formattedValue: String
get() = when (value) {
YES_MANUAL -> "YES_MANUAL"
YES_AUTO -> "YES_AUTO"
NO -> "NO"
SKIP -> "SKIP"
UNKNOWN -> "UNKNOWN"
else -> value.toString()
}
companion object { companion object {
/** /**
* Value indicating that the habit is not applicable for this timestamp. * Value indicating that the habit is not applicable for this timestamp.

@ -22,6 +22,7 @@ import com.opencsv.CSVWriter
import java.io.IOException import java.io.IOException
import java.io.Writer import java.io.Writer
import java.util.LinkedList import java.util.LinkedList
import java.util.Locale
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
/** /**
@ -182,24 +183,34 @@ abstract class HabitList : Iterable<Habit> {
val header = arrayOf( val header = arrayOf(
"Position", "Position",
"Name", "Name",
"Type",
"Question", "Question",
"Description", "Description",
"NumRepetitions", "FrequencyNumerator",
"Interval", "FrequencyDenominator",
"Color" "Color",
"Unit",
"Target Type",
"Target Value",
"Archived?"
) )
val csv = CSVWriter(out) val csv = CSVWriter(out)
csv.writeNext(header, false) csv.writeNext(header, false)
for (habit in this) { for (habit in this) {
val (numerator, denominator) = habit.frequency val (numerator, denominator) = habit.frequency
val cols = arrayOf( val cols = arrayOf(
String.format("%03d", indexOf(habit) + 1), String.format(Locale.US, "%03d", indexOf(habit) + 1),
habit.name, habit.name,
habit.type.name,
habit.question, habit.question,
habit.description, habit.description,
numerator.toString(), numerator.toString(),
denominator.toString(), denominator.toString(),
habit.color.toCsvColor() habit.color.toCsvColor(),
if (habit.isNumerical) habit.unit else "",
if (habit.isNumerical) habit.targetType.name else "",
if (habit.isNumerical) habit.targetValue.toString() else "",
habit.isArchived.toString()
) )
csv.writeNext(cols, false) csv.writeNext(cols, false)
} }

@ -35,6 +35,10 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis
true, false, false, true, true, true, false, false, true, true true, false, false, true, true, true, false, false, true, true
) )
private var NON_DAILY_HABIT_NOTES = arrayOf(
"", "Sick", "Forgot to do it, really", "", "", "", "\"Vacation\"", "", "", ""
)
fun createEmptyHabit( fun createEmptyHabit(
name: String = "Meditate", name: String = "Meditate",
color: PaletteColor = PaletteColor(3), color: PaletteColor = PaletteColor(3),
@ -141,10 +145,10 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis
habit.frequency = Frequency(2, 3) habit.frequency = Frequency(2, 3)
saveIfSQLite(habit) saveIfSQLite(habit)
var timestamp = getToday() var timestamp = getToday()
for (c in NON_DAILY_HABIT_CHECKS) { for (i in NON_DAILY_HABIT_CHECKS.indices) {
var value = Entry.NO var value = Entry.NO
if (c) value = Entry.YES_MANUAL if (NON_DAILY_HABIT_CHECKS[i]) value = Entry.YES_MANUAL
habit.originalEntries.add(Entry(timestamp, value)) habit.originalEntries.add(Entry(timestamp, value, NON_DAILY_HABIT_NOTES[i]))
timestamp = timestamp.minus(1) timestamp = timestamp.minus(1)
} }
habit.recompute() habit.recompute()

@ -18,7 +18,6 @@
*/ */
package org.isoron.uhabits.core.io package org.isoron.uhabits.core.io
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.BaseUnitTest
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
@ -30,6 +29,7 @@ import java.io.IOException
import java.nio.file.Files import java.nio.file.Files
import java.util.* import java.util.*
import java.util.zip.ZipFile import java.util.zip.ZipFile
import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class HabitsCSVExporterTest : BaseUnitTest() { class HabitsCSVExporterTest : BaseUnitTest() {
@ -108,15 +108,12 @@ class HabitsCSVExporterTest : BaseUnitTest() {
private fun assertFileAndReferenceAreEqual(s: String) { private fun assertFileAndReferenceAreEqual(s: String) {
val assetFilename = String.format("csv_export/%s", s) val assetFilename = String.format("csv_export/%s", s)
val file = File.createTempFile("asset", "") val actualFile = File(String.format("%s/%s", baseDir.absolutePath, s))
file.deleteOnExit() val expectedFile = File.createTempFile("asset", "")
copyAssetToFile(assetFilename, file) expectedFile.deleteOnExit()
copyAssetToFile(assetFilename, expectedFile)
assertTrue( val actualContents = actualFile.readText()
FileUtils.contentEquals( val expectedContents = expectedFile.readText()
file, assertEquals(expectedContents, actualContents, "content mismatch for $s")
File(String.format("%s/%s", baseDir.absolutePath, s))
)
)
} }
} }

@ -202,13 +202,16 @@ class HabitListTest : BaseUnitTest() {
h2.description = "" h2.description = ""
h2.frequency = Frequency(2, 3) h2.frequency = Frequency(2, 3)
h2.color = PaletteColor(5) h2.color = PaletteColor(5)
val h3 = fixtures.createNumericalHabit()
list.add(h1) list.add(h1)
list.add(h2) list.add(h2)
list.add(h3)
val expectedCSV = val expectedCSV =
""" """
Position,Name,Question,Description,NumRepetitions,Interval,Color Position,Name,Type,Question,Description,FrequencyNumerator,FrequencyDenominator,Color,Unit,Target Type,Target Value,Archived?
001,Meditate,Did you meditate this morning?,this is a test description,1,1,#FF8F00 001,Meditate,YES_NO,Did you meditate this morning?,this is a test description,1,1,#FF8F00,,,,false
002,Wake up early,Did you wake up before 6am?,,2,3,#AFB42B 002,Run,NUMERICAL,How many miles did you run today?,,1,1,#E64A19,miles,AT_LEAST,2.0,false
003,Wake up early,YES_NO,Did you wake up before 6am?,,2,3,#AFB42B,,,,false
""".trimIndent() """.trimIndent()
val writer = StringWriter() val writer = StringWriter()

Loading…
Cancel
Save