Compare commits

...

11 Commits

Author SHA1 Message Date
9ed4630f9b Merge branch 'release/2.3.0' 2025-06-24 21:25:14 -05:00
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
fa670b19b7 HabitCardView: Fix confetti position in API 36+ 2025-03-22 23:03:59 -05:00
39 changed files with 84 additions and 61 deletions

View File

@@ -1,5 +1,21 @@
# 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
### Added
- Add support for Android 14 (@iSoron, @hiqua)

View File

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

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists

View File

@@ -41,14 +41,13 @@ kotlin {
android {
namespace = "org.isoron.uhabits"
compileSdk = 35
// compileSdkPreview = "VanillaIceCream"
compileSdk = 36
defaultConfig {
versionCode = 20200
versionName = "2.2.0"
versionCode = 20300
versionName = "2.3.0"
minSdk = 28
targetSdk = 35
targetSdk = 36
applicationId = "org.isoron.uhabits"
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.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.utils.getCenter
import org.isoron.uhabits.utils.sres
class CheckmarkDialog : AppCompatDialogFragment() {
var onToggle: (Int, String, Float, Float) -> Unit = { _, _, _, _ -> }
var onToggle: (Int, String) -> Unit = { _, _ -> }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val appComponent = (requireActivity().application as HabitsApplication).component
@@ -64,8 +63,7 @@ class CheckmarkDialog : AppCompatDialogFragment() {
}
fun onClick(v: Int) {
val notes = view.notes.text.toString().trim()
val location = view.yesBtn.getCenter()
onToggle(v, notes, location.x, location.y)
onToggle(v, notes)
requireDialog().dismiss()
}
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }

View File

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

View File

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

View File

@@ -224,6 +224,7 @@ class ListHabitsScreen
}
override fun showConfetti(color: PaletteColor, x: Float, y: Float) {
if (x == 0f && y == 0f) return
if (preferences.isConfettiAnimationDisabled) return
val baseColor = themeSwitcher.currentTheme!!.color(color).toInt()
rootView.get().konfettiView.start(
@@ -268,7 +269,7 @@ class ListHabitsScreen
putDouble("value", value)
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")
}
@@ -286,7 +287,7 @@ class ListHabitsScreen
putInt("value", selectedValue)
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")
}

View File

@@ -169,7 +169,8 @@ class HabitCardView(
}
onEdit = { 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
onEdit = { 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 {
val today = DateUtils.getTodayWithOffset()
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 x = checkmarkPanel.x + button.x + (button.width / 2).toFloat()
val x = panel.x + button.x + (button.width / 2).toFloat()
return PointF(x, y)
}
@@ -234,9 +240,15 @@ class HabitCardView(
val containerLocation = IntArray(2)
this.getLocationOnScreen(containerLocation)
val relButtonLocation = getRelativeButtonLocation(timestamp)
val windowInsets = rootWindowInsets
val statusBarHeight = if (SDK_INT <= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
windowInsets?.systemWindowInsetTop ?: 0
} else {
0
}
return PointF(
containerLocation[0].toFloat() + relButtonLocation.x,
containerLocation[1].toFloat() - relButtonLocation.y
containerLocation[1].toFloat() + relButtonLocation.y - statusBarHeight
)
}

View File

@@ -181,7 +181,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
putDouble("value", value)
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")
}
@@ -198,7 +198,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
putInt("value", selectedValue)
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")
}

View File

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

View File

@@ -69,7 +69,7 @@ abstract class HabitWidgetView : FrameLayout {
val shadowRadius = dpToPixels(context, 2f).toInt()
val shadowOffset = dpToPixels(context, 1f).toInt()
val shadowColor = Color.argb(shadowAlpha, 0, 0, 0)
val cornerRadius = dpToPixels(context, 12f)
val cornerRadius = dpToPixels(context, 18f)
val radii = FloatArray(8)
Arrays.fill(radii, cornerRadius)
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">
<shape android:shape="rectangle">
<corners android:radius="12dp"/>
<corners android:radius="18dp"/>
<solid android:color="?android:colorPrimary"/>
</shape>
<color android:color="@color/white"/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -51,11 +51,11 @@ open class ListHabitsBehavior @Inject constructor(
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!!)
if (habit.type == HabitType.NUMERICAL) {
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()
if (newValue != oldValue) {
if (
@@ -72,7 +72,7 @@ open class ListHabitsBehavior @Inject constructor(
entry.value,
entry.notes,
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)
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes))
}
@@ -159,9 +159,7 @@ open class ListHabitsBehavior @Inject constructor(
fun interface NumberPickerCallback {
fun onNumberPicked(
newValue: Double,
notes: String,
x: Float,
y: Float
notes: String
)
fun onNumberPickerDismissed() {}
}
@@ -169,9 +167,7 @@ open class ListHabitsBehavior @Inject constructor(
fun interface CheckMarkDialogCallback {
fun onNotesSaved(
value: Int,
notes: String,
x: Float,
y: Float
notes: String
)
fun onNotesDismissed() {}
}

View File

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

View File

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