Compare commits

...

9 Commits

Author SHA1 Message Date
70dab74528 Bump version to 2.3.0 and update changelog 2025-06-23 22:23:55 -05:00
7e5d2fa207 Update Gradle and AGP versions to 8.11.1 and 8.9.2 2025-06-23 21:48:00 -05:00
0e432fb332 HistoryWidget: Increase padding; update widget images 2025-06-23 21:47:47 -05:00
897a236501 Widgets: Update test images 2025-03-23 19:41:31 -05:00
0cccecec77 Widgets: Update test images 2025-03-23 19:27:12 -05:00
f1ed875256 Further increase widget corner radius to match current Android style 2025-03-23 17:10:39 -05:00
e82bd47aab Increase minimum widget size to 50x50 and 100x100
Some Samsung phones were allowing graph widgets to occupy 1x2 or
2x1 grid cells, leading to very small text. This commit bumps up
the minimum widget size to 100x100 to ensure they always occupy at
least 2x2 cells. Tested on Pixel 4, Pixel 7 and Samsung Galaxy S24.

Closes #2118
2025-03-23 16:52:23 -05:00
e9517f7378 Bump targetSdk to 36 2025-03-23 07:22:36 -05:00
12cc70a51a Confetti: Always emit from checkmark, not popup button 2025-03-23 07:06:45 -05:00
39 changed files with 77 additions and 60 deletions

View File

@@ -1,5 +1,21 @@
# Changelog # Changelog
## [2.3.0] -- 2025-06-23
### Added
- Add support for Android 15 and 16 (@iSoron)
- Show confetti animation (@gokulk16, @iSoron, #1743)
- Show streaks for measurable habits (@teckwarz, #2059)
- Allow user to unset measurable habits (@leontodd, @kalina559, #1899, #2109)
### Changed
- Change background widget color for habits with implicit checks (@wobbba, #1915)
### Fixed
- Fix notification when goal type is set to maximum (@manish99verma, #1931)
- Never mark "at most" habits as completed for the day (@kalina559, #2077)
- Increase minimum widget size (@iSoron, #2118)
- 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)

View File

@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.8.0" agp = "8.9.2"
annotation = "1.9.1" annotation = "1.9.1"
appcompat = "1.7.0" appcompat = "1.7.0"
appintro = "6.3.1" appintro = "6.3.1"

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -41,14 +41,13 @@ kotlin {
android { android {
namespace = "org.isoron.uhabits" namespace = "org.isoron.uhabits"
compileSdk = 35 compileSdk = 36
// compileSdkPreview = "VanillaIceCream"
defaultConfig { defaultConfig {
versionCode = 20200 versionCode = 20300
versionName = "2.2.0" versionName = "2.3.0"
minSdk = 28 minSdk = 28
targetSdk = 35 targetSdk = 36
applicationId = "org.isoron.uhabits" applicationId = "org.isoron.uhabits"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -33,11 +33,10 @@ import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.databinding.CheckmarkPopupBinding import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.utils.getCenter
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
class CheckmarkDialog : AppCompatDialogFragment() { class CheckmarkDialog : AppCompatDialogFragment() {
var onToggle: (Int, String, Float, Float) -> Unit = { _, _, _, _ -> } var onToggle: (Int, String) -> Unit = { _, _ -> }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val appComponent = (requireActivity().application as HabitsApplication).component val appComponent = (requireActivity().application as HabitsApplication).component
@@ -64,8 +63,7 @@ class CheckmarkDialog : AppCompatDialogFragment() {
} }
fun onClick(v: Int) { fun onClick(v: Int) {
val notes = view.notes.text.toString().trim() val notes = view.notes.text.toString().trim()
val location = view.yesBtn.getCenter() onToggle(v, notes)
onToggle(v, notes, location.x, location.y)
requireDialog().dismiss() requireDialog().dismiss()
} }
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) } view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }

View File

@@ -25,7 +25,7 @@ import java.text.ParseException
class NumberDialog : AppCompatDialogFragment() { class NumberDialog : AppCompatDialogFragment() {
var onToggle: (Double, String, Float, Float) -> Unit = { _, _, _, _ -> } var onToggle: (Double, String) -> Unit = { _, _ -> }
var onDismiss: () -> Unit = {} var onDismiss: () -> Unit = {}
private var originalNotes: String = "" private var originalNotes: String = ""
@@ -122,7 +122,7 @@ class NumberDialog : AppCompatDialogFragment() {
} }
val notes = view.notes.text.toString() val notes = view.notes.text.toString()
val location = view.saveBtn.getCenter() val location = view.saveBtn.getCenter()
onToggle(value, notes, location.x, location.y) onToggle(value, notes)
requireDialog().dismiss() requireDialog().dismiss()
} }
} }

View File

@@ -180,7 +180,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
val timestamp = intent.extras?.getLong("timestamp") val timestamp = intent.extras?.getLong("timestamp")
if (habitId != null && timestamp != null) { if (habitId != null && timestamp != null) {
val habit = appComponent.habitList.getById(habitId)!! val habit = appComponent.habitList.getById(habitId)!!
component.listHabitsBehavior.onEdit(habit, Timestamp(timestamp)) component.listHabitsBehavior.onEdit(habit, Timestamp(timestamp), 0f, 0f)
} }
} }
intent = null intent = null

View File

@@ -224,6 +224,7 @@ 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 (preferences.isConfettiAnimationDisabled) return if (preferences.isConfettiAnimationDisabled) return
val baseColor = themeSwitcher.currentTheme!!.color(color).toInt() val baseColor = themeSwitcher.currentTheme!!.color(color).toInt()
rootView.get().konfettiView.start( rootView.get().konfettiView.start(
@@ -268,7 +269,7 @@ class ListHabitsScreen
putDouble("value", value) putDouble("value", value)
putString("notes", notes) putString("notes", notes)
} }
dialog.onToggle = { v, n, x, y -> callback.onNumberPicked(v, n, x, y) } dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
dialog.dismissCurrentAndShow(fm, "numberDialog") dialog.dismissCurrentAndShow(fm, "numberDialog")
} }
@@ -286,7 +287,7 @@ class ListHabitsScreen
putInt("value", selectedValue) putInt("value", selectedValue)
putString("notes", notes) putString("notes", notes)
} }
dialog.onToggle = { v, n, x, y -> callback.onNotesSaved(v, n, x, y) } dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
dialog.dismissCurrentAndShow(fm, "checkmarkDialog") dialog.dismissCurrentAndShow(fm, "checkmarkDialog")
} }

View File

@@ -169,7 +169,8 @@ class HabitCardView(
} }
onEdit = { timestamp -> onEdit = { timestamp ->
triggerRipple(timestamp) triggerRipple(timestamp)
habit?.let { behavior.onEdit(it, timestamp) } val location = getAbsoluteButtonLocation(timestamp)
habit?.let { behavior.onEdit(it, timestamp, location.x, location.y) }
} }
} }
@@ -177,7 +178,8 @@ class HabitCardView(
visibility = GONE visibility = GONE
onEdit = { timestamp -> onEdit = { timestamp ->
triggerRipple(timestamp) triggerRipple(timestamp)
habit?.let { behavior.onEdit(it, timestamp) } val location = getAbsoluteButtonLocation(timestamp)
habit?.let { behavior.onEdit(it, timestamp, location.x, location.y) }
} }
} }
@@ -224,9 +226,13 @@ class HabitCardView(
private fun getRelativeButtonLocation(timestamp: Timestamp): PointF { private fun getRelativeButtonLocation(timestamp: Timestamp): PointF {
val today = DateUtils.getTodayWithOffset() val today = DateUtils.getTodayWithOffset()
val offset = timestamp.daysUntil(today) - dataOffset val offset = timestamp.daysUntil(today) - dataOffset
val button = checkmarkPanel.buttons[offset] val panel = when (habit!!.isNumerical) {
true -> numberPanel
false -> checkmarkPanel
}
val button = panel.buttons[offset]
val y = button.height / 2.0f val y = button.height / 2.0f
val x = checkmarkPanel.x + button.x + (button.width / 2).toFloat() val x = panel.x + button.x + (button.width / 2).toFloat()
return PointF(x, y) return PointF(x, y)
} }

View File

@@ -181,7 +181,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
putDouble("value", value) putDouble("value", value)
putString("notes", notes) putString("notes", notes)
} }
dialog.onToggle = { v, n, x, y -> callback.onNumberPicked(v, n, x, y) } dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog") dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog")
} }
@@ -198,7 +198,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
putInt("value", selectedValue) putInt("value", selectedValue)
putString("notes", notes) putString("notes", notes)
} }
dialog.onToggle = { v, n, x, y -> callback.onNotesSaved(v, n, x, y) } dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog") dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog")
} }

View File

@@ -75,7 +75,8 @@ class HistoryWidget(
firstWeekday = prefs.firstWeekday, firstWeekday = prefs.firstWeekday,
series = listOf(), series = listOf(),
defaultSquare = HistoryChart.Square.OFF, defaultSquare = HistoryChart.Square.OFF,
notesIndicators = listOf() notesIndicators = listOf(),
padding = 2.5
) )
} }
).apply { ).apply {

View File

@@ -69,7 +69,7 @@ abstract class HabitWidgetView : FrameLayout {
val shadowRadius = dpToPixels(context, 2f).toInt() val shadowRadius = dpToPixels(context, 2f).toInt()
val shadowOffset = dpToPixels(context, 1f).toInt() val shadowOffset = dpToPixels(context, 1f).toInt()
val shadowColor = Color.argb(shadowAlpha, 0, 0, 0) val shadowColor = Color.argb(shadowAlpha, 0, 0, 0)
val cornerRadius = dpToPixels(context, 12f) val cornerRadius = dpToPixels(context, 18f)
val radii = FloatArray(8) val radii = FloatArray(8)
Arrays.fill(radii, cornerRadius) Arrays.fill(radii, cornerRadius)
val shape = RoundRectShape(radii, null, null) val shape = RoundRectShape(radii, null, null)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -28,7 +28,7 @@
<item android:id="@android:id/mask"> <item android:id="@android:id/mask">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<corners android:radius="12dp"/> <corners android:radius="18dp"/>
<solid android:color="?android:colorPrimary"/> <solid android:color="?android:colorPrimary"/>
</shape> </shape>
<color android:color="@color/white"/> <color android:color="@color/white"/>

View File

@@ -19,8 +19,8 @@
--> -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minHeight="40dp" android:minHeight="50dp"
android:minWidth="40dp" android:minWidth="50dp"
android:initialLayout="@layout/widget_wrapper" android:initialLayout="@layout/widget_wrapper"
android:previewImage="@drawable/widget_preview_checkmark" android:previewImage="@drawable/widget_preview_checkmark"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"

View File

@@ -19,10 +19,10 @@
--> -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minHeight="80dp" android:minHeight="100dp"
android:minWidth="80dp" android:minWidth="100dp"
android:minResizeWidth="80dp" android:minResizeWidth="100dp"
android:minResizeHeight="80dp" android:minResizeHeight="100dp"
android:initialLayout="@layout/widget_graph" android:initialLayout="@layout/widget_graph"
android:previewImage="@drawable/widget_preview_frequency" android:previewImage="@drawable/widget_preview_frequency"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"

View File

@@ -19,10 +19,10 @@
--> -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minHeight="80dp" android:minHeight="100dp"
android:minWidth="80dp" android:minWidth="100dp"
android:minResizeWidth="80dp" android:minResizeWidth="100dp"
android:minResizeHeight="80dp" android:minResizeHeight="100dp"
android:initialLayout="@layout/widget_graph" android:initialLayout="@layout/widget_graph"
android:previewImage="@drawable/widget_preview_history" android:previewImage="@drawable/widget_preview_history"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"

View File

@@ -19,10 +19,10 @@
--> -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minHeight="80dp" android:minHeight="100dp"
android:minWidth="80dp" android:minWidth="100dp"
android:minResizeWidth="80dp" android:minResizeWidth="100dp"
android:minResizeHeight="80dp" android:minResizeHeight="100dp"
android:initialLayout="@layout/widget_graph" android:initialLayout="@layout/widget_graph"
android:previewImage="@drawable/widget_preview_score" android:previewImage="@drawable/widget_preview_score"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"

View File

@@ -19,10 +19,10 @@
--> -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minHeight="80dp" android:minHeight="100dp"
android:minWidth="80dp" android:minWidth="100dp"
android:minResizeWidth="80dp" android:minResizeWidth="100dp"
android:minResizeHeight="80dp" android:minResizeHeight="100dp"
android:initialLayout="@layout/widget_graph" android:initialLayout="@layout/widget_graph"
android:previewImage="@drawable/widget_preview_streaks" android:previewImage="@drawable/widget_preview_streaks"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"

View File

@@ -19,10 +19,10 @@
--> -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minHeight="80dp" android:minHeight="100dp"
android:minWidth="80dp" android:minWidth="100dp"
android:minResizeWidth="80dp" android:minResizeWidth="100dp"
android:minResizeHeight="80dp" android:minResizeHeight="100dp"
android:initialLayout="@layout/widget_graph" android:initialLayout="@layout/widget_graph"
android:previewImage="@drawable/widget_preview_target" android:previewImage="@drawable/widget_preview_target"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"

View File

@@ -51,11 +51,11 @@ open class ListHabitsBehavior @Inject constructor(
screen.showHabitScreen(h) screen.showHabitScreen(h)
} }
fun onEdit(habit: Habit, timestamp: Timestamp?) { fun onEdit(habit: Habit, timestamp: Timestamp?, x: Float, y: Float) {
val entry = habit.computedEntries.get(timestamp!!) val entry = habit.computedEntries.get(timestamp!!)
if (habit.type == HabitType.NUMERICAL) { if (habit.type == HabitType.NUMERICAL) {
val oldValue = entry.value.toDouble() / 1000 val oldValue = entry.value.toDouble() / 1000
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String, x: Float, y: Float -> screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String ->
val value = (newValue * 1000).roundToInt() val value = (newValue * 1000).roundToInt()
if (newValue != oldValue) { if (newValue != oldValue) {
if ( if (
@@ -72,7 +72,7 @@ open class ListHabitsBehavior @Inject constructor(
entry.value, entry.value,
entry.notes, entry.notes,
habit.color habit.color
) { newValue: Int, newNotes: String, x: Float, y: Float -> ) { newValue: Int, newNotes: String ->
if (newValue != entry.value && newValue == YES_MANUAL) screen.showConfetti(habit.color, x, y) if (newValue != entry.value && newValue == YES_MANUAL) screen.showConfetti(habit.color, x, y)
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes)) commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes))
} }
@@ -159,9 +159,7 @@ open class ListHabitsBehavior @Inject constructor(
fun interface NumberPickerCallback { fun interface NumberPickerCallback {
fun onNumberPicked( fun onNumberPicked(
newValue: Double, newValue: Double,
notes: String, notes: String
x: Float,
y: Float
) )
fun onNumberPickerDismissed() {} fun onNumberPickerDismissed() {}
} }
@@ -169,9 +167,7 @@ open class ListHabitsBehavior @Inject constructor(
fun interface CheckMarkDialogCallback { fun interface CheckMarkDialogCallback {
fun onNotesSaved( fun onNotesSaved(
value: Int, value: Int,
notes: String, notes: String
x: Float,
y: Float
) )
fun onNotesDismissed() {} fun onNotesDismissed() {}
} }

View File

@@ -98,7 +98,7 @@ class HistoryCardPresenter(
entry.value, entry.value,
entry.notes, entry.notes,
habit.color habit.color
) { newValue, newNotes, _: Float, _: Float -> ) { newValue, newNotes ->
commandRunner.run( commandRunner.run(
CreateRepetitionCommand( CreateRepetitionCommand(
habitList, habitList,
@@ -135,7 +135,7 @@ class HistoryCardPresenter(
screen.showNumberPopup( screen.showNumberPopup(
value = oldValue / 1000.0, value = oldValue / 1000.0,
notes = entry.notes notes = entry.notes
) { newValue: Double, newNotes: String, _: Float, _: Float -> ) { newValue: Double, newNotes: String ->
val thousands = (newValue * 1000).roundToInt() val thousands = (newValue * 1000).roundToInt()
commandRunner.run( commandRunner.run(
CreateRepetitionCommand( CreateRepetitionCommand(

View File

@@ -78,13 +78,13 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
@Test @Test
fun testOnEdit() { fun testOnEdit() {
behavior.onEdit(habit2, getToday()) behavior.onEdit(habit2, getToday(), 0f, 0f)
verify(screen).showNumberPopup( verify(screen).showNumberPopup(
eq(0.1), eq(0.1),
eq(""), eq(""),
picker.capture() picker.capture()
) )
picker.lastValue.onNumberPicked(100.0, "", 0f, 0f) picker.lastValue.onNumberPicked(100.0, "")
val today = getTodayWithOffset() val today = getTodayWithOffset()
assertThat(habit2.computedEntries.get(today).value, equalTo(100000)) assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
} }