Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Comments

Widgets overhaul and changelog#236

Merged
shub39 merged 12 commits intomasterfrom
dev
Feb 18, 2026
Merged

Widgets overhaul and changelog#236
shub39 merged 12 commits intomasterfrom
dev

Conversation

@shub39
Copy link
Owner

@shub39 shub39 commented Feb 18, 2026

Summary by CodeRabbit

  • New Features

    • Habit Week Chart widget for weekly analytics
    • New/rewritten All‑Tasks, Habit Overview and Habit Streak widgets
    • In‑app Changelog dialog to review release notes
  • Improvements

    • Dynamic widget previews for supported launchers
    • Tasks & Habits widgets: multiple UI fixes and refinements
    • Landscape UI setting adjusted; launcher icon change reverted
    • Expanded localization and new UI icons/drawables

Kristof Toth and others added 9 commits February 13, 2026 07:09
Currently translated at 90.4% (123 of 136 strings)

Translation: Grit/strings
Translate-URL: https://hosted.weblate.org/projects/grit/strings/hu/
Signed-off-by: shub39 <[email protected]>
* ✨ habit widget improvements
* ✨ new and improved streak widget
* ✨ changed notif icon and replaced refresh/arrow_forward icon…
* ✨ testing out widget previews
* ✨ added widget previews
* ✨ tweaked widget proportions
* ✨ appropriate renaming
* ✨ habit week chart widget
* ✨ removed faulty average metric
Currently translated at 86.0% (117 of 136 strings)

Translation: Grit/strings
Translate-URL: https://hosted.weblate.org/projects/grit/strings/zh_Hans/
Currently translated at 91.9% (125 of 136 strings)

Translation: Grit/strings
Translate-URL: https://hosted.weblate.org/projects/grit/strings/ru/
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds an in-app changelog system (asset, manager, UI, datastore tracking), restructures Glance widgets into dedicated packages (AllTasks, HabitOverview, HabitStreak, HabitWeekChart), updates widget metadata/receivers, renames habit APIs, removes server-port datastore keys, bumps app version, and adds resources/strings.

Changes

Cohort / File(s) Summary
Changelog feature
CHANGELOG.md, app/src/main/assets/changelog.json, app/src/main/java/com/shub39/grit/core/data/ChangelogManager.kt, app/src/main/java/com/shub39/grit/core/presentation/ChangelogDialog.kt, app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Changelog.kt, app/src/main/java/com/shub39/grit/core/domain/MainAppState.kt, app/src/main/java/com/shub39/grit/viewmodels/MainViewModel.kt, app/src/main/java/com/shub39/grit/viewmodels/SettingsViewModel.kt, app/src/main/java/com/shub39/grit/core/presentation/settings/SettingsState.kt
Adds static changelog.json asset, ChangelogManager (loads asset into StateFlow), UI (dialog + settings screen), MainAppState field, viewmodel wiring to show/persist last-shown changelog.
Widgets — reorganized & new
app/src/main/java/com/shub39/grit/widgets/..., app/src/main/res/xml/..._widget.xml, app/src/main/AndroidManifest.xml
Removes legacy single-file widget implementations; introduces modular widget packages and receivers for AllTasks, HabitOverview, HabitStreak, HabitWeekChart; updates manifest receiver entries and provider XML resources.
AllTasks widget (new package)
app/src/main/java/com/shub39/grit/widgets/all_tasks_widget/AllTasksWidget.kt, app/src/main/java/com/shub39/grit/widgets/all_tasks_widget/AllTasksWidgetReceiver.kt, app/src/main/res/xml/all_tasks_widget.xml
Adds Glance AllTasks widget, receiver, and updated provider XML with refined size/resize attributes and shorter update interval.
HabitOverview / HabitStreak / HabitWeekChart
app/src/main/java/com/shub39/grit/widgets/habit_overview_widget/..., .../habit_streak_widget/..., .../habit_weekchart_widget/..., corresponding res/xml and res/values/strings.xml
Adds new Glance widget implementations and receivers for habit overview, streak, and week-chart; provides provider XMLs and new string resources; removes older widget implementations and v31 XML variants.
Datastore & API surface
app/src/main/java/com/shub39/grit/core/data/DataStoreImpl.kt, app/src/main/java/com/shub39/grit/core/domain/GritDatastore.kt
Removes serverPort preferences and accessors; introduces lastChangelogShown key and APIs getLastChangelogShown() / updateLastChangelogShown(version).
Habit repo / viewmodel changes
shared/core/src/commonMain/.../HabitRepo.kt, app/src/main/java/.../habits/data/repository/HabitRepository.kt, app/src/main/java/.../viewmodels/HabitViewModel.kt
Renames getHabitStatus()getHabitsWithAnalytics(), adds getHabitsWithStatus(), renames deleteHabitStatus parameter; updates consumers (ViewModel).
App wiring & previews
app/src/main/java/com/shub39/grit/app/App.kt, app/src/main/java/com/shub39/grit/app/GritApplication.kt, app/src/main/java/com/shub39/grit/app/MainActivity.kt
App composable gains onDismissChangelog, shows ChangelogDialog; MainActivity forwards dismiss; Application sets Glance widget previews for supported API levels at startup.
Build & assets
app/build.gradle.kts, app/src/main/assets/changelog.json, app/src/main/java/com/shub39/grit/widgets/WidgetSize.kt
Bumps appVersion to 5.6.0 (5600), adds generateChangelogJson Gradle task and static changelog asset, and introduces WidgetSize constants.
Resources & localization
app/src/main/res/drawable/*.xml, app/src/main/res/xml/*.xml, app/src/main/res/values/strings.xml, shared/core/src/commonMain/composeResources/values*/strings.xml
Adds multiple vector and shape drawables, new widget provider XMLs, new string keys (e.g., changelog, habit_streak_widget, habit_week_chart_widget, nothing_to_show), and updates HU/RU/ZH translations.
Notifications & deps
app/src/main/java/com/shub39/grit/core/presentation/util.kt, gradle/libs.versions.toml
Notification icons switched to R.drawable.notif_icon; various dependency version bumps (AGP, compose-charts, purchases, KSP, navigation, compose-junit).

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant App as App
    participant MainVM as MainViewModel
    participant ChangelogMgr as ChangelogManager
    participant DataStore as DataStore
    participant Dialog as ChangelogDialog

    User->>App: Launch
    App->>MainVM: onStart
    MainVM->>ChangelogMgr: request changelogs
    ChangelogMgr->>ChangelogMgr: read assets/changelog.json
    ChangelogMgr-->>MainVM: changelog list
    MainVM->>DataStore: getLastChangelogShown()
    DataStore-->>MainVM: last shown version
    MainVM->>MainVM: compare versions
    alt new version available
        MainVM->>DataStore: updateLastChangelogShown(newVersion)
        MainVM-->>App: set currentChangelog
        App->>Dialog: show(currentChangelog)
        User->>Dialog: dismiss
        Dialog->>App: onDismissChangelog()
        App->>MainVM: dismissChangelog()
        MainVM->>MainVM: clear currentChangelog
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • task-widget #157: Directly related — introduces/changes the AllTasks Glance widget and receiver wiring.
  • server #171: Related — touches datastore server-port APIs which were removed/replaced here with changelog tracking.
  • vico-stats #169: Related — renames/adjusts habit analytics APIs used by new widgets and viewmodels.

Poem

🐰 I hopped through changelogs, carrots in tow,

Widgets rearranged in tidy rows to show,
Week charts and streaks now sparkle and glow,
Strings and icons stitched where new winds blow,
A little rabbit cheers for five-dot-six-oh!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Widgets overhaul and changelog' accurately reflects the two major aspects of this changeset: comprehensive widget restructuring (removal of old widgets, creation of new ones in reorganized packages, dynamic previews) and the addition of a changelog feature (ChangelogManager, ChangelogDialog, changelog.json asset, UI integration).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (15)
app/src/main/res/drawable/check_list.xml (1)

8-8: Hardcoded fill color won't adapt to dark/dynamic themes.

#B7B7B7 is baked in at the drawable level, so the icon will always render as static gray regardless of the active light/dark theme or Material You dynamic color. Prefer a theme attribute or @color resource so the tint adapts automatically.

🎨 Suggested fix
-      android:fillColor="#B7B7B7"/>
+      android:fillColor="@color/widget_icon_tint"/>

Define the token in res/values/colors.xml (and a dark-mode override in res/values-night/colors.xml):

<!-- res/values/colors.xml -->
<color name="widget_icon_tint">#B7B7B7</color>

<!-- res/values-night/colors.xml -->
<color name="widget_icon_tint">#E0E0E0</color>

Alternatively, if this drawable is always used inside an ImageView or AppCompatImageView, setting android:tint="?attr/colorControlNormal" at the usage site (or via a DrawableCompat.setTint call) achieves the same result without touching the drawable itself.

Given the summary notes that several other drawables (alarm.xml, analytics.xml, arrow_forward.xml, check_circle.xml, circle_border.xml, heat.xml, heat_outlined.xml, refresh.xml, view_week.xml) were also added in this PR, the same fix should be applied consistently across all of them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/drawable/check_list.xml` at line 8, The drawable uses a
hardcoded android:fillColor="#B7B7B7" which prevents theme/dark-mode adaptation;
replace hardcoded literals in check_list.xml (and the other added drawables:
alarm.xml, analytics.xml, arrow_forward.xml, check_circle.xml,
circle_border.xml, heat.xml, heat_outlined.xml, refresh.xml, view_week.xml) with
a theme-aware reference such as `@color/widget_icon_tint` or an attribute like
?attr/colorControlNormal, and add corresponding color resources in
res/values/colors.xml and res/values-night/colors.xml (or ensure the attribute
is provided by your theme) so the icon tint adapts to light/dark/dynamic themes.
app/src/main/res/drawable/check_circle.xml (1)

8-8: Consider using a color resource or theme attribute instead of a hardcoded hex color.

#B7B7B7 won't adapt to light/dark theming. If the app already has a designated icon tint color (or uses dynamic theming), referencing it here keeps all icons consistent and automatically responsive to theme changes.

🎨 Proposed change using a color resource

Define the color once in res/values/colors.xml:

+ <color name="icon_tint_default">#B7B7B7</color>

Then reference it in the drawable:

-      android:fillColor="#B7B7B7"/>
+      android:fillColor="@color/icon_tint_default"/>

Alternatively, if the app supports theming via ?attr/, you could bind to a theme attribute so the icon color shifts automatically between light and dark modes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/drawable/check_circle.xml` at line 8, Replace the hardcoded
hex fillColor in check_circle.xml (android:fillColor="#B7B7B7") with a color
resource or theme attribute so the icon adapts to theming; create or reuse a
color in res/values/colors.xml (e.g., ic_tint) or reference a theme attribute
(e.g., ?attr/iconTint) and update the android:fillColor in the drawable to use
that resource reference instead.
app/src/main/res/drawable/heat.xml (1)

8-8: Consider replacing the hardcoded fill color with a color resource or theme attribute.

#B7B7B7 is a static value that won't respond to dark/light theme switches. Using a named color resource (e.g., @color/icon_tint_default) or a theme attribute (e.g., ?attr/colorOnSurface) allows the icon to adapt automatically. Alternatively, leave the drawable untinted and apply app:tint at each usage site.

♻️ Proposed change
-      android:fillColor="#B7B7B7"/>
+      android:fillColor="?attr/colorOnSurface"/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/drawable/heat.xml` at line 8, The drawable hardcodes a
static fill color ("android:fillColor=\"#B7B7B7\"") which prevents theme-aware
tinting; replace that literal with a color resource or theme attribute (e.g.,
use a named color like `@color/icon_tint_default` or a theme attribute like
?attr/colorOnSurface) or remove the fillColor attribute so the vector can be
tinted at usage via app:tint; update the heat.xml vector's android:fillColor to
reference the chosen resource/attribute or delete the attribute to enable
runtime tinting.
app/src/main/res/drawable/refresh.xml (1)

7-8: Consider using a theme-aware tint instead of a hardcoded fill color.

android:fillColor="#B7B7B7" is a fixed gray that won't respond to light/dark mode switching or Material You dynamic color. For widget icons in particular, the system theme can vary independently of the app theme, making adaptive coloring especially important.

♻️ Proposed refactor — switch to a theme-attributed tint
  <path
-     android:pathData="M480,800q-134,0 -227,-93t-93,-227..."
-     android:fillColor="#B7B7B7"/>
+     android:pathData="M480,800q-134,0 -227,-93t-93,-227..."
+     android:fillColor="@android:color/white"/>

Then apply the tint at the call site (e.g., in the widget RemoteViews) via setColorFilter, or define a dedicated color resource (e.g., @color/icon_on_widget) and reference it here — keeping the path fill neutral and controlling color externally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/drawable/refresh.xml` around lines 7 - 8, The drawable
hardcodes android:fillColor="#B7B7B7" (in refresh.xml) which prevents
theme-aware coloring; remove or neutralize the fillColor (leave path fill
transparent or use a neutral color), and switch to tinting the vector at runtime
by applying a theme attribute or color resource (e.g., `@color/icon_on_widget` or
?attr/colorControlNormal) from the widget/RemoteViews—use
setColorFilter/setImageViewTintList or setImageViewBitmap with a tinted
bitmap—so the icon responds to light/dark/Material You; keep the existing
pathData intact and only change the fill handling and call sites that set the
tint.
app/src/main/res/drawable/arrow_forward.xml (1)

8-8: Replace hardcoded fill color with a theme attribute to ensure the icon adapts across dark and light themes.

The hardcoded color #B7B7B7 appears in at least six usage locations without tint applied (ChangelogDialog.kt, Changelog.kt, and multiple instances in RootPage.kt). This causes the icon to display as a fixed gray regardless of the active theme.

🎨 Proposed refactor
-      android:fillColor="#B7B7B7"/>
+      android:fillColor="?attr/colorOnSurfaceVariant"/>

This ensures the color adapts automatically without relying on tint application at each usage site.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/drawable/arrow_forward.xml` at line 8, Replace the hardcoded
fillColor="#B7B7B7" in arrow_forward.xml with a theme attribute so the drawable
adapts to dark/light themes; locate the android:fillColor attribute in
arrow_forward.xml and change it to reference an appropriate theme color
attribute (e.g., ?attr/colorOnSurface or
?attr/colorControlNormal/textColorSecondary depending on your design system),
then ensure usages (ChangelogDialog.kt, Changelog.kt, RootPage.kt) rely on the
drawable's theme-aware color rather than per-use tinting.
app/src/main/res/drawable/heat_outlined.xml (1)

8-8: All drawable icons in this project use the same hardcoded gray color—this is a consistent pattern, not an oversight.

The hardcoded #B7B7B7 appears in all 10 drawable XML files (alarm.xml, analytics.xml, arrow_forward.xml, check_circle.xml, check_list.xml, circle_border.xml, heat.xml, heat_outlined.xml, refresh.xml, view_week.xml). The project's color resources (colors.xml) currently define only Material Design palette colors (purples, teals, black, white) with no gray icon color token.

If the project is planned to support dark/light theming in future, all icon drawables should be refactored together to use a theme attribute or shared color resource, not individually. This would be a coordinated, project-wide change rather than isolated to this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/drawable/heat_outlined.xml` at line 8, The drawables
(including heat_outlined.xml) currently hardcode android:fillColor="#B7B7B7";
change these to reference a shared token instead of hardcoding—replace
android:fillColor with either a theme attribute (e.g., ?attr/iconColor) or a
color resource (e.g., `@color/icon_gray`) and update all 10 drawable files
(alarm.xml, analytics.xml, arrow_forward.xml, check_circle.xml, check_list.xml,
circle_border.xml, heat.xml, heat_outlined.xml, refresh.xml, view_week.xml)
consistently; then add the new color resource in colors.xml or declare the
attribute in your theme so icons will follow light/dark theming.
app/src/main/java/com/shub39/grit/habits/data/repository/HabitRepository.kt (1)

136-146: Missing .flowOn(Dispatchers.Default) for consistency and thread safety.

getHabitsWithAnalytics() uses .flowOn(Dispatchers.Default) to keep its combine/map work off the main thread. getHabitsWithStatus() performs equivalent CPU-bound work without that constraint, leaving it at the mercy of the collector's dispatcher.

♻️ Proposed fix
 override fun getHabitsWithStatus(): Flow<List<Pair<Habit, Boolean>>> {
     return habits.combine(habitStatuses) { habitsFlow, statusFlow ->
         habitsFlow.map { habit ->
             val dates = statusFlow
                 .filter { it.habitId == habit.id }
                 .map { it.date }

             habit to dates.any { it == LocalDate.now() }
         }
-    }
+    }.flowOn(Dispatchers.Default)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/grit/habits/data/repository/HabitRepository.kt`
around lines 136 - 146, The getHabitsWithStatus() implementation does CPU-bound
combine/map work but lacks thread confinement; update the function
(getHabitsWithStatus) to apply .flowOn(Dispatchers.Default) to the returned Flow
so the combine/map runs off the main thread, and add an import for
kotlinx.coroutines.Dispatchers if necessary; ensure the return type remains
Flow<List<Pair<Habit, Boolean>>> and place .flowOn(Dispatchers.Default) after
the combine block.
app/src/main/java/com/shub39/grit/widgets/WidgetSize.kt (1)

5-9: data object adds unnecessary data-class semantics to a constants holder.

Plain object suffices since toString, equals, and hashCode generated by data object serve no purpose here.

♻️ Proposed fix
-data object WidgetSize {
+object WidgetSize {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/grit/widgets/WidgetSize.kt` around lines 5 - 9,
Replace the unnecessary data object with a plain singleton object: change the
declaration of WidgetSize from a `data object` to `object WidgetSize` so the
constants (Width2, Width4, Height1, Height2) remain accessible but no data-class
semantics (generated equals/hashCode/toString) are created for this constants
holder; update any imports/usages if needed to reflect the object name remains
the same.
app/src/main/java/com/shub39/grit/widgets/habit_overview_widget/HabitOverviewWidget.kt (1)

77-95: updateAll races ahead of the DB write, momentarily showing stale data.

onUpdateHabit launches coroutine A (DB write) and onUpdateWidget independently launches coroutine B (updateAll). Coroutine B typically finishes first, causing the widget to re-render from the Flow's current (pre-write) state before the DB write completes. Move updateAll inside the DB-write coroutine so it fires only after the mutation is committed.

♻️ Proposed fix
-onUpdateHabit = { habitWithStatus ->
-    scope.launch {
-        if (habitWithStatus.second) {
-            repo.deleteHabitStatus(
-                habitId = habitWithStatus.first.id,
-                date = LocalDate.now()
-            )
-        } else {
-            repo.insertHabitStatus(
-                habitStatus = HabitStatus(
-                    habitId = habitWithStatus.first.id,
-                    date = LocalDate.now()
-                )
-            )
-        }
-    }
-},
-onUpdateWidget = {
-    scope.launch { [email protected](context) }
-}
+onUpdateHabit = { habitWithStatus ->
+    scope.launch {
+        if (habitWithStatus.second) {
+            repo.deleteHabitStatus(
+                habitId = habitWithStatus.first.id,
+                date = LocalDate.now()
+            )
+        } else {
+            repo.insertHabitStatus(
+                habitStatus = HabitStatus(
+                    habitId = habitWithStatus.first.id,
+                    date = LocalDate.now()
+                )
+            )
+        }
+        [email protected](context)
+    }
+},
+onUpdateWidget = {
+    scope.launch { [email protected](context) }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/shub39/grit/widgets/habit_overview_widget/HabitOverviewWidget.kt`
around lines 77 - 95, The widget update races because onUpdateHabit and
onUpdateWidget launch separate coroutines; change onUpdateHabit so the same
coroutine that calls repo.deleteHabitStatus or repo.insertHabitStatus also calls
[email protected](context) after the suspend DB call completes
(i.e., inside the scope.launch in the onUpdateHabit lambda), ensuring updateAll
runs only after repo.deleteHabitStatus / repo.insertHabitStatus finish rather
than launching an independent coroutine.
app/src/main/java/com/shub39/grit/core/domain/GritDatastore.kt (1)

51-52: Minor naming inconsistency: update vs set convention.

All other setter methods in this interface use the set* prefix (e.g., setAppTheme, setSeedColor, setAmoledPref), but this one uses updateLastChangelogShown. Consider renaming to setLastChangelogShown for consistency, or vice versa if update is the preferred naming going forward.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/grit/core/domain/GritDatastore.kt` around lines
51 - 52, The method name updateLastChangelogShown is inconsistent with the rest
of the interface’s setter naming (setAppTheme, setSeedColor, setAmoledPref);
rename updateLastChangelogShown to setLastChangelogShown (and update all call
sites and implementations of GritDatastore along with any tests) so the
getter/getter pair is getLastChangelogShown / setLastChangelogShown and naming
is consistent across the interface.
app/src/main/java/com/shub39/grit/widgets/habit_streak_widget/HabitStreakWidget.kt (1)

96-107: Cycling logic is safe due to UI gating, but consider a guard.

When sortedData is empty, currentIndex is 0 and sortedData.size - 1 is -1, so the else branch would call sortedData[1] causing an IndexOutOfBoundsException. This is safe in practice because the arrow button is only rendered when habitWithAnalytics != null (Line 181). However, a defensive check would prevent a crash if the UI structure changes.

🛡️ Defensive guard
                         onChangeHabit = {
+                            if (sortedData.isEmpty()) return@Content
                             val nextId = if (currentIndex == sortedData.size - 1) {
                                 sortedData.first().habit.id
                             } else {
                                 sortedData[currentIndex + 1].habit.id
                             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/shub39/grit/widgets/habit_streak_widget/HabitStreakWidget.kt`
around lines 96 - 107, Add a defensive guard in the onChangeHabit block to
handle an empty sortedData before computing nextId: check that
sortedData.isNotEmpty() (or sortedData.firstOrNull() != null) and that
currentIndex is within bounds, and if not, return early (or skip calling
updateHabitId). Specifically update the onChangeHabit lambda that computes
nextId using currentIndex and sortedData, ensuring you only call scope.launch {
updateHabitId(context, id, nextId) } when a valid nextId was determined from
sortedData to avoid IndexOutOfBoundsException.
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Changelog.kt (1)

75-108: Consider providing stable keys for lazy list items.

The forEach + nested item/items approach works but lacks stable keys. If the changelog data ever changes during the composable's lifetime (or for animation/state restoration), keyed items are safer. Since the data is static today this is a minor hardening.

Suggested improvement
             changelog.forEach { versionEntry ->
                 item(key = "header-${versionEntry.version}") {
                     Text(
                         text = "# ${versionEntry.version}",
                         ...
                     )
                 }
 
-                items(versionEntry.changes) { change ->
+                items(
+                    items = versionEntry.changes,
+                    key = { "${versionEntry.version}-$it" }
+                ) { change ->
                     ...
                 }
 
                 item(key = "spacer-${versionEntry.version}") {
                     Spacer(modifier = Modifier.height(16.dp))
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Changelog.kt`
around lines 75 - 108, Replace the current changelog.forEach + nested item/items
usage with keyed lazy list entries so items have stable keys: iterate the
top-level list using LazyListScope.items(changelog, key = { it.version })
(instead of changelog.forEach) so each version header and its content are tied
to the version key, and for the inner changes use items(versionEntry.changes,
key = { change /* or index if duplicates possible */ }) (instead of
items(versionEntry.changes) without keys) or supply item(key =
versionEntry.version) for the header; update references to the existing item and
items(...) calls accordingly to ensure stable keys for both version entries and
change rows.
app/src/main/java/com/shub39/grit/app/GritApplication.kt (1)

36-42: Unhandled exceptions will silently cancel the coroutine and skip remaining widget previews.

If setWidgetPreviews throws for any receiver (e.g., missing widget metadata), subsequent receivers in the same coroutine won't get their previews set. Consider wrapping each call or launching independently.

Also, MainScope() is a fire-and-forget scope here—acceptable for Application, but if you ever need structured shutdown, store the scope reference and cancel it.

Suggested improvement
-            `@SuppressLint`("CheckResult")
-            MainScope().launch {
-                manager.setWidgetPreviews(HabitOverviewWidgetReceiver::class)
-                manager.setWidgetPreviews(HabitStreakWidgetReceiver::class)
-                manager.setWidgetPreviews(AllTasksWidgetReceiver::class)
-                manager.setWidgetPreviews(HabitWeekChartWidgetReceiver::class)
-            }
+            val widgetReceivers = listOf(
+                HabitOverviewWidgetReceiver::class,
+                HabitStreakWidgetReceiver::class,
+                AllTasksWidgetReceiver::class,
+                HabitWeekChartWidgetReceiver::class,
+            )
+            `@SuppressLint`("CheckResult")
+            MainScope().launch {
+                widgetReceivers.forEach { receiver ->
+                    try {
+                        manager.setWidgetPreviews(receiver)
+                    } catch (e: Exception) {
+                        android.util.Log.e("GritApplication", "Failed to set widget preview for $receiver", e)
+                    }
+                }
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/grit/app/GritApplication.kt` around lines 36 -
42, The coroutine launched with MainScope().launch calls
manager.setWidgetPreviews for multiple receivers and will silently stop if any
call throws; change to either wrap each
manager.setWidgetPreviews(HabitOverviewWidgetReceiver::class),
manager.setWidgetPreviews(HabitStreakWidgetReceiver::class),
manager.setWidgetPreviews(AllTasksWidgetReceiver::class),
manager.setWidgetPreviews(HabitWeekChartWidgetReceiver::class) in individual
try/catch blocks to log/handle exceptions so one failure doesn't cancel the
rest, or launch separate coroutines per receiver (e.g., scope.launch { ... }) so
failures are isolated; also persist the MainScope() as a field (instead of
fire-and-forget) so you can cancel it during shutdown if you need structured
lifecycle management.
app/src/main/java/com/shub39/grit/viewmodels/SettingsViewModel.kt (1)

138-146: getChangeLogs() is already called from within onStart, which runs in the flow collection context — the extra viewModelScope.launch is redundant but harmless.

The onStart block already executes in a coroutine context (it's a flow operator). The inner viewModelScope.launch creates a separate coroutine that runs independently, meaning if the subscriber disappears immediately, this coroutine still completes. This is consistent with the existing observeJob() pattern on Line 148, so no change is strictly needed, but worth noting for awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/grit/viewmodels/SettingsViewModel.kt` around
lines 138 - 146, getChangeLogs() launches a redundant coroutine inside a caller
that is already in a flow context; remove the inner viewModelScope.launch and
perform the work directly so the call runs in the existing coroutine (i.e.,
replace the viewModelScope.launch block in getChangeLogs() with a direct
_state.update that assigns changelog = changelogManager.changelogs.first()),
keeping the same use of _state.update and changelogManager.changelogs.first();
if callers expect a suspending call, make getChangeLogs a suspend function or
ensure callers invoke it from a coroutine context consistent with observeJob().
app/src/main/java/com/shub39/grit/viewmodels/MainViewModel.kt (1)

122-122: In DEBUG builds, the changelog dialog will always appear on every app launch.

The condition BuildConfig.DEBUG || lastShownChangelog != BuildConfig.VERSION_NAME short-circuits in debug mode, so the dialog shows every time regardless of the persisted version. If this is intentional for development/testing, consider adding a brief comment to clarify intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/shub39/grit/viewmodels/MainViewModel.kt` at line 122,
Split the current combined condition in MainViewModel so intent is explicit:
replace the single if (BuildConfig.DEBUG || lastShownChangelog !=
BuildConfig.VERSION_NAME) with two branches — one for debug-only behavior (if
(BuildConfig.DEBUG) { /* show changelog for testing */ }) and a separate branch
checking persisted version (else if (lastShownChangelog !=
BuildConfig.VERSION_NAME) { ... }), or if you prefer to keep a single condition,
add a clear comment next to the condition explaining that showing the changelog
on every debug launch is intentional for testing; reference the
BuildConfig.DEBUG flag, lastShownChangelog variable and BuildConfig.VERSION_NAME
in your change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/build.gradle.kts`:
- Around line 158-216: The generated JSON may contain unescaped backslashes
because the current escaping only handles quotes at the point where item is
processed in the generateChangelogJson task (see the use of item.replace("\"",
"\\\"")); update the escape logic to first replace backslashes then quotes
(e.g., replace "\" with "\\\\" before escaping quotes) or switch to a proper
JSON serializer to build the changelog objects, ensuring the transformation is
applied inside the loop that constructs each entry's "changes" strings.

In `@app/src/main/assets/changelog.json`:
- Around line 3-12: changelog.json's "5.6.0" changes array is missing the entry
"Fixed settings UI on landscape mode"; run the Gradle task generateChangelogJson
(e.g., ./gradlew :app:generateChangelogJson) to regenerate
app/src/main/assets/changelog.json so the version "5.6.0" block in
changelog.json matches CHANGELOG.md and includes that missing entry.

In `@app/src/main/java/com/shub39/grit/core/data/ChangelogManager.kt`:
- Around line 20-29: getChangelogs() currently does synchronous file I/O and
JSON parsing on the collector dispatcher and has no error handling; change it to
perform the asset read and Json.decodeFromString<Changelog>(...) inside
withContext(Dispatchers.IO) (or otherwise on an IO dispatcher), wrap the
read+parse in a try/catch to handle
FileNotFoundException/IOException/SerializationException, and on error post a
safe fallback (e.g., an empty/default Changelog) to _changelogs.update; ensure
only the IO work is moved to Dispatchers.IO and that you still call
_changelogs.update from a coroutine context safe for state updates.

In
`@app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt`:
- Around line 384-388: Replace the hardcoded "Changelog" literal in the
headlineContent Text inside RootPage.kt with the localized string resource;
locate the Text call in the headlineContent lambda (the Text composable
rendering "Changelog") and change it to use stringResource(Res.string.changelog)
(Res.string.changelog is already defined and imported in the sibling
Changelog.kt), ensuring the proper import for
androidx.compose.ui.res.stringResource is present.

In `@app/src/main/java/com/shub39/grit/viewmodels/MainViewModel.kt`:
- Around line 117-129: The current checkChangelog() both sets currentChangelog
in state and calls datastore.updateLastChangelogShown(BuildConfig.VERSION_NAME),
which may persist the version before the UI is shown; remove the persistence
from checkChangelog() so it only sets _state.update { it.copy(currentChangelog =
changeLogs.firstOrNull()) } (using changelogManager.changelogs.first() and
BuildConfig.VERSION_NAME as before), and instead perform
datastore.updateLastChangelogShown(BuildConfig.VERSION_NAME) inside
dismissChangelog() when the user actually closes/dismisses the dialog; ensure
dismissChangelog() clears currentChangelog from state and then calls
datastore.updateLastChangelogShown(...) so the shown flag is only saved after
user acknowledgment.

In
`@app/src/main/java/com/shub39/grit/widgets/habit_streak_widget/HabitStreakWidget.kt`:
- Around line 255-295: Replace all hardcoded user-facing strings in
HabitStreakWidget.kt (the Text calls that currently use "Current Streak", "Best
Streak", and the " Day " suffix) with string resources: add keys like
current_streak, best_streak, and day_suffix (or a formatted string with a %d
placeholder) to res/values/strings.xml, then update the Text invocations to use
context.getString(R.string.current_streak),
context.getString(R.string.best_streak), and
context.getString(R.string.day_suffix) (or
context.getString(R.string.day_with_count, habitWithAnalytics.currentStreak)) so
the streak number is formatted via getString rather than concatenating literal "
Day ". Ensure you use the same context instance used elsewhere in this file (the
one used for context.getString at line ~391) or obtain LocalContext when needed.
- Around line 360-378: In the narrow-layout branch of the HabitStreakWidget (the
Column that renders the best-streak Texts), the secondary-background content is
incorrectly using GlanceTheme.colors.onPrimary for the
“${habitWithAnalytics.bestStreak} Day ” and/or the "Best Streak" Text; change
those Text style color usages to GlanceTheme.colors.onSecondary to match the
wide-width branch and ensure readable contrast (locate the Column/Text blocks in
HabitStreakWidget.kt that render the best streak in the narrow branch and swap
onPrimary -> onSecondary in their TextStyle color properties).

In
`@app/src/main/java/com/shub39/grit/widgets/habit_weekchart_widget/HabitWeekChartWidget.kt`:
- Around line 227-262: In HabitWeekChartWidget.kt, fix the division-by-zero and
premature rounding in the bar height calculation: compute maxData as now but if
maxData == 0.0 treat the scale ratio as 0.0 to avoid NaN (or use
maxData.coerceAtLeast(epsilon) for a safe denominator), calculate the height
using the raw double (e.g., ratio = double / maxData) rather than
double.roundToInt(), and only round for the displayed label (keep Text using
double.roundToInt() or formatted value); update the expression in the
Box.height(...) inside the data.forEach block to use the safe ratio-based
computation so heights are stable and proportional when values are small or all
zeros.

In `@app/src/main/res/drawable/analytics.xml`:
- Line 8: Replace the hardcoded android:fillColor="#B7B7B7" in analytics.xml
with a theme-aware or named color resource: either reference a theme attribute
like ?attr/colorOnSurface (or another appropriate Material attribute) to support
light/dark/dynamic color, or extract the hex into a named color in colors.xml
(e.g., `@color/neutral_grey`) and reference that; update any usages to use the new
resource/attribute so the icon adapts to theme changes.

In `@app/src/main/res/drawable/arrow_forward.xml`:
- Around line 1-5: The vector drawable for the arrow_forward icon needs RTL
mirroring: add android:autoMirrored="true" to the root <vector> element so the
icon flips automatically in RTL locales; locate the <vector
xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:viewportWidth="960" android:viewportHeight="960">
and update it to include android:autoMirrored="true".

In `@app/src/main/res/drawable/heat_outlined.xml`:
- Around line 6-8: The path element in heat_outlined.xml is missing the
android:fillType="evenOdd" attribute causing inner voids to render filled;
update the <path> (the element with android:pathData starting "M239,801.19...")
to include android:fillType="evenOdd" alongside android:fillColor so the
outlined flame's internal cutouts render correctly.

In `@app/src/main/res/xml/habit_streak_widget.xml`:
- Around line 5-13: The widget currently sets android:maxResizeHeight equal to
android:minHeight/android:minResizeHeight, which makes vertical resizing a
no-op; either (A) if fixed height is intended, change
android:resizeMode="horizontal|vertical" to android:resizeMode="horizontal" so
vertical resize is disabled, or (B) if vertical resize should be allowed,
increase android:maxResizeHeight (for example to "422dp" like in
all_tasks_widget.xml) so there is a positive vertical resize range; update the
android:maxResizeHeight attribute (and android:maxResizeWidth if needed)
accordingly while keeping android:minHeight and android:minResizeHeight as-is.

In `@app/src/main/res/xml/habit_week_chart_widget.xml`:
- Around line 5-13: The widget's vertical resize is ineffective because
android:maxResizeHeight ("132dp") equals android:minHeight and
android:minResizeHeight (all "132dp"), so adjust the XML: either increase
android:maxResizeHeight to a larger dp value (e.g., >132dp) to permit vertical
expansion, or change android:resizeMode from "horizontal|vertical" to
"horizontal" only; update the attributes android:maxResizeHeight,
android:minHeight, android:minResizeHeight and android:resizeMode in
habit_week_chart_widget.xml accordingly so the chosen behavior (resizable
vertically or fixed vertically) is enforced.

In `@shared/core/src/commonMain/composeResources/values-hu/strings.xml`:
- Line 117: The localized string with key "weeks" in values-hu (string
name="weeks") was changed to "Hét" (singular) but likely needs the Hungarian
plural "Hetek" for contexts like "X weeks ago" or plural unit labels; check
where the "weeks" resource is used (e.g., timeline/relative-time formatting,
unit labels, Compose components referencing R.string.weeks) and if it is
displayed as a plural/unit or with a numeric prefix, revert the translation back
to "Hetek" (or use the appropriate plural resource) to match the usage context.

In `@shared/core/src/commonMain/composeResources/values-ru/strings.xml`:
- Line 139: The string resource server_channel_description starts with a
lowercase letter; update its value to begin with an uppercase letter to match
other channel/description strings (e.g., make "канал для отправки серверу
статуса обновлений" into "Канал для отправки серверу статуса обновлений") so the
resource key server_channel_description follows the same capitalization style as
channel_description and similar entries.

In `@shared/core/src/commonMain/composeResources/values-zh-rCN/strings.xml`:
- Line 120: The translation for the string resource with name "weeks" currently
uses "周内", which implies "within the week" and may not match the intended
neutral plural label; update the value of the <string name="weeks"> entry to a
more appropriate neutral translation such as "周" or "数周" so the label correctly
conveys "weeks" as a standalone term.

---

Nitpick comments:
In `@app/src/main/java/com/shub39/grit/app/GritApplication.kt`:
- Around line 36-42: The coroutine launched with MainScope().launch calls
manager.setWidgetPreviews for multiple receivers and will silently stop if any
call throws; change to either wrap each
manager.setWidgetPreviews(HabitOverviewWidgetReceiver::class),
manager.setWidgetPreviews(HabitStreakWidgetReceiver::class),
manager.setWidgetPreviews(AllTasksWidgetReceiver::class),
manager.setWidgetPreviews(HabitWeekChartWidgetReceiver::class) in individual
try/catch blocks to log/handle exceptions so one failure doesn't cancel the
rest, or launch separate coroutines per receiver (e.g., scope.launch { ... }) so
failures are isolated; also persist the MainScope() as a field (instead of
fire-and-forget) so you can cancel it during shutdown if you need structured
lifecycle management.

In `@app/src/main/java/com/shub39/grit/core/domain/GritDatastore.kt`:
- Around line 51-52: The method name updateLastChangelogShown is inconsistent
with the rest of the interface’s setter naming (setAppTheme, setSeedColor,
setAmoledPref); rename updateLastChangelogShown to setLastChangelogShown (and
update all call sites and implementations of GritDatastore along with any tests)
so the getter/getter pair is getLastChangelogShown / setLastChangelogShown and
naming is consistent across the interface.

In
`@app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Changelog.kt`:
- Around line 75-108: Replace the current changelog.forEach + nested item/items
usage with keyed lazy list entries so items have stable keys: iterate the
top-level list using LazyListScope.items(changelog, key = { it.version })
(instead of changelog.forEach) so each version header and its content are tied
to the version key, and for the inner changes use items(versionEntry.changes,
key = { change /* or index if duplicates possible */ }) (instead of
items(versionEntry.changes) without keys) or supply item(key =
versionEntry.version) for the header; update references to the existing item and
items(...) calls accordingly to ensure stable keys for both version entries and
change rows.

In `@app/src/main/java/com/shub39/grit/habits/data/repository/HabitRepository.kt`:
- Around line 136-146: The getHabitsWithStatus() implementation does CPU-bound
combine/map work but lacks thread confinement; update the function
(getHabitsWithStatus) to apply .flowOn(Dispatchers.Default) to the returned Flow
so the combine/map runs off the main thread, and add an import for
kotlinx.coroutines.Dispatchers if necessary; ensure the return type remains
Flow<List<Pair<Habit, Boolean>>> and place .flowOn(Dispatchers.Default) after
the combine block.

In `@app/src/main/java/com/shub39/grit/viewmodels/MainViewModel.kt`:
- Line 122: Split the current combined condition in MainViewModel so intent is
explicit: replace the single if (BuildConfig.DEBUG || lastShownChangelog !=
BuildConfig.VERSION_NAME) with two branches — one for debug-only behavior (if
(BuildConfig.DEBUG) { /* show changelog for testing */ }) and a separate branch
checking persisted version (else if (lastShownChangelog !=
BuildConfig.VERSION_NAME) { ... }), or if you prefer to keep a single condition,
add a clear comment next to the condition explaining that showing the changelog
on every debug launch is intentional for testing; reference the
BuildConfig.DEBUG flag, lastShownChangelog variable and BuildConfig.VERSION_NAME
in your change.

In `@app/src/main/java/com/shub39/grit/viewmodels/SettingsViewModel.kt`:
- Around line 138-146: getChangeLogs() launches a redundant coroutine inside a
caller that is already in a flow context; remove the inner viewModelScope.launch
and perform the work directly so the call runs in the existing coroutine (i.e.,
replace the viewModelScope.launch block in getChangeLogs() with a direct
_state.update that assigns changelog = changelogManager.changelogs.first()),
keeping the same use of _state.update and changelogManager.changelogs.first();
if callers expect a suspending call, make getChangeLogs a suspend function or
ensure callers invoke it from a coroutine context consistent with observeJob().

In
`@app/src/main/java/com/shub39/grit/widgets/habit_overview_widget/HabitOverviewWidget.kt`:
- Around line 77-95: The widget update races because onUpdateHabit and
onUpdateWidget launch separate coroutines; change onUpdateHabit so the same
coroutine that calls repo.deleteHabitStatus or repo.insertHabitStatus also calls
[email protected](context) after the suspend DB call completes
(i.e., inside the scope.launch in the onUpdateHabit lambda), ensuring updateAll
runs only after repo.deleteHabitStatus / repo.insertHabitStatus finish rather
than launching an independent coroutine.

In
`@app/src/main/java/com/shub39/grit/widgets/habit_streak_widget/HabitStreakWidget.kt`:
- Around line 96-107: Add a defensive guard in the onChangeHabit block to handle
an empty sortedData before computing nextId: check that sortedData.isNotEmpty()
(or sortedData.firstOrNull() != null) and that currentIndex is within bounds,
and if not, return early (or skip calling updateHabitId). Specifically update
the onChangeHabit lambda that computes nextId using currentIndex and sortedData,
ensuring you only call scope.launch { updateHabitId(context, id, nextId) } when
a valid nextId was determined from sortedData to avoid
IndexOutOfBoundsException.

In `@app/src/main/java/com/shub39/grit/widgets/WidgetSize.kt`:
- Around line 5-9: Replace the unnecessary data object with a plain singleton
object: change the declaration of WidgetSize from a `data object` to `object
WidgetSize` so the constants (Width2, Width4, Height1, Height2) remain
accessible but no data-class semantics (generated equals/hashCode/toString) are
created for this constants holder; update any imports/usages if needed to
reflect the object name remains the same.

In `@app/src/main/res/drawable/arrow_forward.xml`:
- Line 8: Replace the hardcoded fillColor="#B7B7B7" in arrow_forward.xml with a
theme attribute so the drawable adapts to dark/light themes; locate the
android:fillColor attribute in arrow_forward.xml and change it to reference an
appropriate theme color attribute (e.g., ?attr/colorOnSurface or
?attr/colorControlNormal/textColorSecondary depending on your design system),
then ensure usages (ChangelogDialog.kt, Changelog.kt, RootPage.kt) rely on the
drawable's theme-aware color rather than per-use tinting.

In `@app/src/main/res/drawable/check_circle.xml`:
- Line 8: Replace the hardcoded hex fillColor in check_circle.xml
(android:fillColor="#B7B7B7") with a color resource or theme attribute so the
icon adapts to theming; create or reuse a color in res/values/colors.xml (e.g.,
ic_tint) or reference a theme attribute (e.g., ?attr/iconTint) and update the
android:fillColor in the drawable to use that resource reference instead.

In `@app/src/main/res/drawable/check_list.xml`:
- Line 8: The drawable uses a hardcoded android:fillColor="#B7B7B7" which
prevents theme/dark-mode adaptation; replace hardcoded literals in
check_list.xml (and the other added drawables: alarm.xml, analytics.xml,
arrow_forward.xml, check_circle.xml, circle_border.xml, heat.xml,
heat_outlined.xml, refresh.xml, view_week.xml) with a theme-aware reference such
as `@color/widget_icon_tint` or an attribute like ?attr/colorControlNormal, and
add corresponding color resources in res/values/colors.xml and
res/values-night/colors.xml (or ensure the attribute is provided by your theme)
so the icon tint adapts to light/dark/dynamic themes.

In `@app/src/main/res/drawable/heat_outlined.xml`:
- Line 8: The drawables (including heat_outlined.xml) currently hardcode
android:fillColor="#B7B7B7"; change these to reference a shared token instead of
hardcoding—replace android:fillColor with either a theme attribute (e.g.,
?attr/iconColor) or a color resource (e.g., `@color/icon_gray`) and update all 10
drawable files (alarm.xml, analytics.xml, arrow_forward.xml, check_circle.xml,
check_list.xml, circle_border.xml, heat.xml, heat_outlined.xml, refresh.xml,
view_week.xml) consistently; then add the new color resource in colors.xml or
declare the attribute in your theme so icons will follow light/dark theming.

In `@app/src/main/res/drawable/heat.xml`:
- Line 8: The drawable hardcodes a static fill color
("android:fillColor=\"#B7B7B7\"") which prevents theme-aware tinting; replace
that literal with a color resource or theme attribute (e.g., use a named color
like `@color/icon_tint_default` or a theme attribute like ?attr/colorOnSurface) or
remove the fillColor attribute so the vector can be tinted at usage via
app:tint; update the heat.xml vector's android:fillColor to reference the chosen
resource/attribute or delete the attribute to enable runtime tinting.

In `@app/src/main/res/drawable/refresh.xml`:
- Around line 7-8: The drawable hardcodes android:fillColor="#B7B7B7" (in
refresh.xml) which prevents theme-aware coloring; remove or neutralize the
fillColor (leave path fill transparent or use a neutral color), and switch to
tinting the vector at runtime by applying a theme attribute or color resource
(e.g., `@color/icon_on_widget` or ?attr/colorControlNormal) from the
widget/RemoteViews—use setColorFilter/setImageViewTintList or setImageViewBitmap
with a tinted bitmap—so the icon responds to light/dark/Material You; keep the
existing pathData intact and only change the fill handling and call sites that
set the tint.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Changelog.kt (2)

38-41: Consider renaming to ChangelogScreen to avoid shadowing the Changelog type alias.

The composable function name Changelog is identical to the imported type alias Changelog = List<VersionEntry> (line 26). While Kotlin resolves this by context, it hampers readability at call sites and in navigation graphs. A name like ChangelogScreen would disambiguate cleanly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Changelog.kt`
around lines 38 - 41, Rename the composable function Changelog to
ChangelogScreen to avoid shadowing the Changelog type alias (Changelog =
List<VersionEntry>). Update the function declaration public symbol
Changelog(...) to ChangelogScreen(...), and update all call sites and navigation
references that invoke Changelog (including any NavGraph routes, composable()
usages, and imports) to use ChangelogScreen so the type alias and composable are
no longer identically named.

74-106: Provide stable keys for LazyColumn items to improve recomposition efficiency.

The item and items calls inside the forEach loop lack keys. If the changelog list changes (e.g., new version prepended), LazyColumn will recompose all items instead of only the diff. Using the version string and change text as keys makes diffing efficient and also preserves scroll state.

Proposed fix
             changelog.forEach { versionEntry ->
-                item {
+                item(key = "header-${versionEntry.version}") {
                     Text(
                         text = versionEntry.version,
                         style = MaterialTheme.typography.headlineLarge.copy(
                             fontWeight = FontWeight.Bold
                         )
                     )
                 }

-                items(versionEntry.changes) { change ->
+                items(
+                    items = versionEntry.changes,
+                    key = { "${versionEntry.version}-$it" }
+                ) { change ->
                     Row(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Changelog.kt`
around lines 74 - 106, The LazyColumn entries produced inside changelog.forEach
lack stable keys; update the header and spacer item calls to use item(key =
versionEntry.version) and the changes list to use items(versionEntry.changes,
key = { change -> change }) (or a composite key like
"${versionEntry.version}-$change" if change text may duplicate) so headers,
change rows and spacers have stable keys tied to versionEntry.version and
change; adjust the spacer item to a keyed item (e.g., key =
"spacer-${versionEntry.version}") to ensure correct diffing and preserved scroll
state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Changelog.kt`:
- Around line 56-59: The Icon's contentDescription is hardcoded ("Navigate
Back"); replace it with a localized stringResource call (e.g.,
stringResource(R.string.navigate_back) or the project's
Res.strings.navigate_back variant) so the back-button uses the same i18n pattern
as other text in this file (look for other uses of stringResource in
Changelog.kt to match style) and add the navigate_back entry to the app's
strings resources if it doesn't exist.

---

Nitpick comments:
In
`@app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Changelog.kt`:
- Around line 38-41: Rename the composable function Changelog to ChangelogScreen
to avoid shadowing the Changelog type alias (Changelog = List<VersionEntry>).
Update the function declaration public symbol Changelog(...) to
ChangelogScreen(...), and update all call sites and navigation references that
invoke Changelog (including any NavGraph routes, composable() usages, and
imports) to use ChangelogScreen so the type alias and composable are no longer
identically named.
- Around line 74-106: The LazyColumn entries produced inside changelog.forEach
lack stable keys; update the header and spacer item calls to use item(key =
versionEntry.version) and the changes list to use items(versionEntry.changes,
key = { change -> change }) (or a composite key like
"${versionEntry.version}-$change" if change text may duplicate) so headers,
change rows and spacers have stable keys tied to versionEntry.version and
change; adjust the spacer item to a keyed item (e.g., key =
"spacer-${versionEntry.version}") to ensure correct diffing and preserved scroll
state.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/shub39/grit/widgets/habit_streak_widget/HabitStreakWidget.kt`:
- Around line 187-206: In HabitStreakWidget.kt the two interactive Image
composables (the refresh image that calls onUpdateWidget and the arrow_forward
image that calls onChangeHabit then onUpdateWidget) set contentDescription =
null — change them to meaningful descriptions (e.g., "Refresh" and "Next habit")
by supplying contentDescription strings (preferably from string resources like
R.string.refresh and R.string.next_habit) so screen readers announce the
controls; keep the existing clickable handlers (onUpdateWidget, onChangeHabit)
unchanged.

In
`@app/src/main/java/com/shub39/grit/widgets/habit_weekchart_widget/HabitWeekChartWidget.kt`:
- Around line 115-138: The preview provider providePreview currently calls
provideContent { Content(...) } without the GlanceTheme wrapper, so theme colors
used inside Content (GlanceTheme.colors.*) are missing; wrap the Content call
inside GlanceTheme { ... } in providePreview to mirror provideGlance's theming
(i.e., change providePreview to call provideContent { GlanceTheme { Content(...)
} }) so the preview uses the same theme as the real widget.

---

Duplicate comments:
In
`@app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt`:
- Around line 368-394: The review indicates the changelog ListItem is correct
(uses stringResource(Res.string.changelog), detachedItemShape(), and
onNavigateToChangelog()), but contains a duplicate approval comment; remove the
duplicate review comment or mark the conversation as resolved so there is a
single approval for the changelog ListItem change.

In `@app/src/main/java/com/shub39/grit/viewmodels/MainViewModel.kt`:
- Around line 117-146: Remove the duplicate review annotation by deleting the
stray "[duplicate_comment]" marker from the PR comment; keep the implemented
behavior as-is (checkChangelog() only updates _state and dismissChangelog()
persists BuildConfig.VERSION_NAME after dismissal), and confirm no code changes
are needed to functions checkChangelog(), dismissChangelog(), or
checkSubscription().

In
`@app/src/main/java/com/shub39/grit/widgets/habit_weekchart_widget/HabitWeekChartWidget.kt`:
- Line 240: The bar height calculation is rounding the data too early: replace
the use of double.roundToInt() inside the ratio with the raw floating value so
the ratio is computed with full precision (i.e. change the expression used in
the .height call from (90 * (double.roundToInt() / maxData)).dp to compute 90 *
(double / maxData) and only round if/where needed after multiplying), ensuring
you use double.toFloat() or cast appropriately for .dp units and optionally
clamp the result to a non-negative range; update the code around the .height((90
* (double.roundToInt() / maxData)).dp) call in HabitWeekChartWidget to use
double / maxData instead of double.roundToInt().

Comment on lines +187 to +206
Box(GlanceModifier.padding(start = 16.dp)) {
Image(
provider = ImageProvider(R.drawable.refresh),
contentDescription = null,
colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface),
modifier = GlanceModifier.clickable { onUpdateWidget() }
)
}
Spacer(modifier = GlanceModifier.width(4.dp))
Box(GlanceModifier.padding(end = 16.dp)) {
Image(
provider = ImageProvider(R.drawable.arrow_forward),
contentDescription = null,
colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface),
modifier = GlanceModifier.clickable {
onChangeHabit()
onUpdateWidget()
}
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Interactive images lack contentDescription — weak accessibility.

Both the refresh (Line 190) and arrow_forward (Line 199) images are interactive (clickable) but have contentDescription = null. Screen readers will have no announcement for these controls. While this same pattern exists in HabitWeekChartWidget, adding concise descriptions (e.g., "Refresh" / "Next habit") would improve accessibility.

♿ Proposed improvement
                         Image(
                             provider = ImageProvider(R.drawable.refresh),
-                            contentDescription = null,
+                            contentDescription = context.getString(R.string.refresh),
                             colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface),
                             modifier = GlanceModifier.clickable { onUpdateWidget() }
                         )
                     }
                     Spacer(modifier = GlanceModifier.width(4.dp))
                     Box(GlanceModifier.padding(end = 16.dp)) {
                         Image(
                             provider = ImageProvider(R.drawable.arrow_forward),
-                            contentDescription = null,
+                            contentDescription = context.getString(R.string.next_habit),
                             colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/shub39/grit/widgets/habit_streak_widget/HabitStreakWidget.kt`
around lines 187 - 206, In HabitStreakWidget.kt the two interactive Image
composables (the refresh image that calls onUpdateWidget and the arrow_forward
image that calls onChangeHabit then onUpdateWidget) set contentDescription =
null — change them to meaningful descriptions (e.g., "Refresh" and "Next habit")
by supplying contentDescription strings (preferably from string resources like
R.string.refresh and R.string.next_habit) so screen readers announce the
controls; keep the existing clickable handlers (onUpdateWidget, onChangeHabit)
unchanged.

Comment on lines +115 to +138
override suspend fun providePreview(context: Context, widgetCategory: Int) {
provideContent {
Content(
onUpdateWidget = {},
onChangeHabit = {},
habitWithAnalytics = HabitWithAnalytics(
habit = Habit(
id = 1,
title = "Exercise",
description = "40 mins daily",
time = LocalDateTime.now(),
days = setOf(),
index = 1,
reminder = false
),
statuses = listOf(),
weeklyComparisonData = (0..7).map { it.toDouble() },
weekDayFrequencyData = mapOf(),
currentStreak = 12,
bestStreak = 20,
startedDaysAgo = 100
)
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

providePreview is missing the GlanceTheme {} wrapper present in provideGlance.

Content references GlanceTheme.colors.* throughout. Without the wrapper in providePreview, the widget picker preview will fall back to un-themed default colors, potentially misrepresenting the widget's actual appearance.

🐛 Proposed fix
 override suspend fun providePreview(context: Context, widgetCategory: Int) {
     provideContent {
+        GlanceTheme {
             Content(
                 onUpdateWidget = {},
                 onChangeHabit = {},
                 habitWithAnalytics = HabitWithAnalytics(
                     ...
                 )
             )
+        }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override suspend fun providePreview(context: Context, widgetCategory: Int) {
provideContent {
Content(
onUpdateWidget = {},
onChangeHabit = {},
habitWithAnalytics = HabitWithAnalytics(
habit = Habit(
id = 1,
title = "Exercise",
description = "40 mins daily",
time = LocalDateTime.now(),
days = setOf(),
index = 1,
reminder = false
),
statuses = listOf(),
weeklyComparisonData = (0..7).map { it.toDouble() },
weekDayFrequencyData = mapOf(),
currentStreak = 12,
bestStreak = 20,
startedDaysAgo = 100
)
)
}
override suspend fun providePreview(context: Context, widgetCategory: Int) {
provideContent {
GlanceTheme {
Content(
onUpdateWidget = {},
onChangeHabit = {},
habitWithAnalytics = HabitWithAnalytics(
habit = Habit(
id = 1,
title = "Exercise",
description = "40 mins daily",
time = LocalDateTime.now(),
days = setOf(),
index = 1,
reminder = false
),
statuses = listOf(),
weeklyComparisonData = (0..7).map { it.toDouble() },
weekDayFrequencyData = mapOf(),
currentStreak = 12,
bestStreak = 20,
startedDaysAgo = 100
)
)
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/shub39/grit/widgets/habit_weekchart_widget/HabitWeekChartWidget.kt`
around lines 115 - 138, The preview provider providePreview currently calls
provideContent { Content(...) } without the GlanceTheme wrapper, so theme colors
used inside Content (GlanceTheme.colors.*) are missing; wrap the Content call
inside GlanceTheme { ... } in providePreview to mirror provideGlance's theming
(i.e., change providePreview to call provideContent { GlanceTheme { Content(...)
} }) so the preview uses the same theme as the real widget.

@shub39 shub39 merged commit 7f1de6d into master Feb 18, 2026
2 of 3 checks passed
This was referenced Feb 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants