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

Skip to content

Conversation

@shub39
Copy link
Owner

@shub39 shub39 commented Nov 14, 2025

Summary by CodeRabbit

  • New Features

    • Added web application support via Kotlin Multiplatform, enabling access to Grit across browser and desktop platforms.
  • Chores

    • Restructured project architecture to support multiplatform development; consolidated core modules and improved dependency injection; updated build system and Gradle configuration; migrated string resources to centralized management system.

@coderabbitai
Copy link

coderabbitai bot commented Nov 14, 2025

Code Review Analysis

Walkthrough

This is a major structural refactoring that consolidates domain and presentation layers into a shared Kotlin Multiplatform core module (shared/core), renames state/action classes for consistency, migrates string resources to a centralized generated resource system, and establishes multi-platform targets (Wasm, JVM, Android) with platform-specific implementations via expect/actual declarations. A new web module is added to support browser deployment.

Changes

Cohort / File(s) Summary
Build Configuration & Dependencies
build.gradle.kts, gradle.gradle.kts, gradle/libs.versions.toml, settings.gradle.kts, app/build.gradle.kts, shared/core/build.gradle.kts, web/build.gradle.kts
Updated Gradle to support multiplatform builds; introduced Compose Multiplatform, kotlinMultiplatform, and Wasm plugins; added version catalog entries for new platforms and libraries (koin, kotlinx-datetime); configured Wasm/JS and desktop targets; increased daemon memory.
Core Domain Layer Migration
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/, shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/domain/
Relocated domain entities (Habit, HabitStatus, Category, Task, etc.) from app module to shared core; added @Serializable annotations; changed package namespaces to reflect core location.
Presentation Layer Refactoring
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/, shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/
Renamed action types (HabitsPageActionHabitsAction, TaskPageActionTaskAction) and state types (HabitPageStateHabitState, TaskPageStateTaskState); created new action/state files in shared core; introduced new HabitsGraph and TasksGraph navigation components.
UI Components & Screens
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/*, shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/*, shared/core/src/commonMain/kotlin/com/shub39/grit/core/*/presentation/ui/section/*
Migrated habit and task UI components and screens to shared core; refactored action callbacks to use new action types; updated resource access to generated Res.string.* references; introduced expect/actual declarations for platform-specific implementations (permissions, UI behavior).
Shared UI Components
shared/core/src/commonMain/kotlin/com/shub39/grit/core/shared_ui/*
Created shared UI utilities (Empty, GritBottomSheet, GritDialog, PageFill) moved from app presentation layer; updated package paths to centralize common UI.
Multiplatform Expect/Actual
shared/core/src/androidMain/, shared/core/src/jvmMain/, shared/core/src/wasmJsMain/kotlin/com/shub39/grit/core/*/
Added platform-specific implementations for HabitUpsertSheet, TaskUpsertSheet, and blurPossible across Android, JVM, and Wasm targets; Android version handles runtime permissions, others provide simplified implementations.
App Module Updates
app/src/main/java/com/shub39/grit/app/Grit.kt, app/src/main/java/com/shub39/grit/app/MainActivity.kt, app/src/main/java/com/shub39/grit/viewmodels/*, app/src/main/java/com/shub39/grit/di/GritModules.kt
Updated Grit composable to use dependency injection for SettingsViewModel; refactored ViewModels to use new action/state types from core; updated DI bindings to reference core domain types; changed action method names to onAction.
Data & Utilities
app/src/main/java/com/shub39/grit/core/data/*, app/src/main/java/com/shub39/grit/core/domain/, shared/core/src/commonMain/kotlin/com/shub39/grit/core/utils/, app/src/main/java/com/shub39/grit/core/presentation/util.kt
Updated all data mappers, repositories, and utilities to import domain types from core package; refactored AppTheme to use StringResource instead of Int; consolidated time/utility functions in core.utils.
Repositories & Server
app/src/main/java/com/shub39/grit/habits/data/repository/*, app/src/main/java/com/shub39/grit/tasks/data/repository/*, app/src/main/java/com/shub39/grit/server/
Updated all repositories to import domain types from core package; adjusted import paths in server domain types and response models.
Old Presentation Files (Deleted)
app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt, app/src/main/java/com/shub39/grit/habits/presentation/HabitsPageAction.kt, app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt, app/src/main/java/com/shub39/grit/tasks/presentation/TaskPageAction.kt, app/src/main/java/com/shub39/grit/tasks/presentation/TaskPageState.kt
Removed legacy navigation graphs, action types, and state types from app module; functionality moved to shared core with renamed types.
Resource Management
app/src/main/res/values/strings.xml, shared/core/src/commonMain/composeResources/values/strings.xml, shared/core/src/commonMain/composeResources/values-es/strings.xml, shared/core/src/commonMain/composeResources/values-uk/strings.xml
Migrated string resources from app Android resources to centralized Compose generated resources system; app resources now minimal; UI code updated to use Res.string.* throughout.
App Module Settings & Infrastructure
.github/FUNDING.yml, .github/workflows/web.yml, .gitignore, app/src/foss/java/com/shub39/grit/billing/PaywallPage.kt, app/src/main/java/com/shub39/grit/widgets/*
Updated funding configuration; added GitHub Pages deployment workflow for Wasm builds; expanded build ignore patterns; updated PaywallPage and widget imports to use generated resources; refactored widget date handling to use kotlinx-datetime.
Web Module (New)
web/build.gradle.kts, web/src/commonMain/kotlin/App.kt, web/src/commonMain/kotlin/domain/StateProvider.kt, web/src/commonMain/kotlin/data/DummyStateProvider.kt, web/src/commonMain/kotlin/di/appModules.kt, web/src/commonMain/kotlin/di/initKoin.kt, web/src/jvmMain/kotlin/main.kt
Created new web module targeting Wasm/JS and JVM; includes App composable with navigation and routing, DI setup with Koin, in-memory state provider for testing/demo purposes, and JVM desktop entry point.

Sequence Diagram(s)

sequenceDiagram
    participant OldApp as App Module
    participant OldDomain as habits/tasks<br/>domain
    participant OldPresentation as habits/tasks<br/>presentation
    
    participant NewShared as shared/core<br/>Module
    participant NewDomain as core.habits/tasks<br/>domain
    participant NewPresentation as core.habits/tasks<br/>presentation
    participant NewViewModels as App ViewModels

    rect rgb(200, 220, 240)
    note over OldApp,OldPresentation: OLD ARCHITECTURE
    OldApp->>OldDomain: import Habit, Task
    OldApp->>OldPresentation: import HabitsPageAction, HabitPageState
    OldPresentation->>OldDomain: uses domain types
    end

    rect rgb(220, 240, 200)
    note over NewShared,NewPresentation: NEW ARCHITECTURE
    NewShared->>NewDomain: provides core types
    NewShared->>NewPresentation: provides HabitsAction, HabitState
    NewViewModels->>NewPresentation: imports actions/states
    NewViewModels->>NewDomain: imports domain types
    NewApp[New App Module]->>NewShared: depends on shared/core
    NewApp->>NewViewModels: updated DI references
    end

    rect rgb(240, 220, 200)
    note over OldApp: Deleted Files
    OldApp--xOldPresentation: HabitsPageAction.kt removed
    OldApp--xOldPresentation: TaskPageAction.kt removed
    OldApp--xOldPresentation: HabitsGraph.kt removed
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Key complexity drivers:

  • Massive structural refactoring: Domain and presentation layers relocated across module boundaries with package namespace changes throughout the codebase (100+ files affected)
  • Architectural shift to multiplatform: New Kotlin Multiplatform setup with expect/actual declarations across three targets (Android, JVM, Wasm); each requires careful platform-specific implementation review
  • State/Action type renaming cascade: HabitsPageActionHabitsAction, HabitPageStateHabitState changes propagate through ViewModels, UI components, and DI; requires tracking all usages for consistency
  • Resource system migration: Shift from Android R.string.* to Compose generated Res.string.* across 50+ files with resource string consolidation; risk of missing string references or breakage
  • New module introduction: Web module adds significant new surface (App.kt, StateProvider, DummyStateProvider, DI setup) requiring validation of navigation, state flow, and DI configuration
  • Permission abstraction: Platform-specific permission handling via expect/actual needs careful review, especially Android's runtime permission flow integration
  • Data layer impact: Repository and mapper updates across app/habits/tasks/server to reference relocated core domain types; must verify no missed imports or type mismatches
  • Heterogeneous edits: Mix of deletions, relocations, renames, and new code; each cohort requires different reasoning; limited repetitive patterns reduce mechanical review efficiency

Areas requiring special attention during review:

  • Verify all HabitsPageAction and TaskPageAction usages have been replaced with new action types throughout app module and ViewModels; check for any dangling references
  • Validate DI bindings in GritModules.kt and new appModules.kt correctly wire repository types from core package
  • Cross-reference all stringResource() calls to ensure they now use Res.string.* and no old R.string.* references remain in compiled code
  • Review platform-specific expect/actual implementations for HabitUpsertSheet, TaskUpsertSheet, and blurPossible to confirm Android permission flow and JVM/Wasm fallbacks are correct
  • Validate StateProvider and DummyStateProvider analytics calculations (streak counting, line chart data, weekday frequency) for correctness
  • Confirm new Wasm target build configuration and Compose resources setup are sound
  • Review navigation graph changes in HabitsGraph and TasksGraph to ensure route transitions and back navigation work correctly
  • Spot-check widget and backup/restore functionality after domain type relocations

Possibly related PRs

  • 🔧 reorganisation #134: Performs similar UI navigation and component namespace reorganization, renaming composables to *Graph variants and adjusting package imports—direct structural alignment with this PR's refactoring approach.
  • dev #144: Modifies the same domain/UI/notification surface (Habit/Task data types, upsert sheets, AlarmScheduler/NotificationReceiver) to add reminder/task scheduling features; this PR refactors/relocates those entities into shared core as a prerequisite.
  • 🔧 added toggle buttons for time period #145: Touches the same habit analytics UI (WeeklyTimePeriod, WeeklyActivity/WeeklyGraph, toggle controls); this PR relocates and renames those files into shared/core while preserving analytics logic.

Poem

🐰 Hopping through packages, we find the way—
Domain types shuffled to core today!
Habits and Tasks in a shared embrace,
Multiplatform magic fills every space.
From Android to Web, the code takes flight,
Refactored and ready—a hoppy delight! 🌟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title 'web-ui' is vague and does not clearly describe the primary changes in this substantial refactoring. Use a more descriptive title that captures the main objectives, such as 'Refactor to monorepo structure with Compose Multiplatform' or 'Migrate to shared core module and add web UI'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 web-ui

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.

@shub39 shub39 merged commit 3e4c84c into master Nov 14, 2025
1 of 2 checks passed
@shub39 shub39 deleted the web-ui branch November 14, 2025 13:09
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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.kt (1)

66-283: TaskUpsertSheet structure is good, but the time picker can never be shown

The expect fun TaskUpsertSheet surface and the split into TaskUpsertSheetContent with notificationPermission/showDateTimePicker/updateDateTimePickerVisibility/onPermissionRequest are reasonable, but the time picker wiring has a functional gap:

  • showTimePicker is initialized to false and never set to true.
  • The “Select Time” IconButton inside DatePickerDialog calls updateDateTimePickerVisibility(true), which is a no-op if the date-time picker is already visible, and does not flip showTimePicker.
  • As a result, TimePickerDialog is never rendered, so users cannot adjust the reminder time; only the default rememberTimePickerState value is used.

You likely want the schedule icon to open the time picker and keep the date picker visible, and the time dialog to close itself via showTimePicker = false. For example:

@@
-    if (showDateTimePicker) {
-        var showTimePicker by remember { mutableStateOf(false) }
+    if (showDateTimePicker) {
+        var showTimePicker by remember { mutableStateOf(false) }
@@
-            dismissButton = {
-                IconButton(
-                    onClick = { updateDateTimePickerVisibility(true) }
-                ) {
+            dismissButton = {
+                IconButton(
+                    onClick = { showTimePicker = true }
+                ) {
                     Icon(
                         imageVector = Icons.Rounded.Schedule,
                         contentDescription = "Select Time"
@@
-            if (showTimePicker) {
-                TimePickerDialog(
-                    onDismissRequest = {},
+            if (showTimePicker) {
+                TimePickerDialog(
+                    onDismissRequest = { showTimePicker = false },
@@
-                        TextButton(
-                            onClick = { showTimePicker = false }
-                        ) {
+                        TextButton(
+                            onClick = { showTimePicker = false }
+                        ) {
                             Text(stringResource(Res.string.done))

This keeps showDateTimePicker under the caller’s control while making the time picker actually usable.

🧹 Nitpick comments (25)
app/src/main/java/com/shub39/grit/widgets/HabitStreakWidget.kt (1)

155-156: Date calculation logic is correct.

The migration to kotlinx-datetime is implemented correctly:

  • Clock.System.todayIn(TimeZone.currentSystemDefault()) properly gets the current local date
  • today.minus(it, DateTimeUnit.DAY) correctly computes past dates
  • The .reversed() ensures chronological ordering (oldest to newest)

Note: Since this is inside a @Composable, the date calculation runs on every recomposition. While the performance impact is negligible for 5 iterations, wrapping these in remember {} would follow Compose best practices for pure computations.

app/src/main/java/com/shub39/grit/server/GritServerRepositoryImpl.kt (1)

7-7: Import path aligned with core refactor; minor startServer cleanup

Line [7]: Importing TaskRepo from com.shub39.grit.core.tasks.domain matches the new shared core layout and keeps this repository wired to the correct domain interface; behavior is unchanged.

Lines [87-89]: val port = port just shadows the function parameter and is only used once in the subsequent embeddedServer call. You can drop the extra val and pass the parameter directly to avoid the shadowing/warning:

-            val port = port
-
-            server = embeddedServer(CIO, host = "0.0.0.0", port = port) {
+            server = embeddedServer(CIO, host = "0.0.0.0", port = port) {

Also applies to: 87-89

app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (1)

33-37: Resource access migrated to generated Res.string API

Lines [33-37,115,133]: Using stringResource(Res.string.bmc) and stringResource(Res.string.rate_on_play) with the generated Res namespace is consistent with the project-wide resource migration and keeps this composable ready for multiplatform reuse, assuming those keys exist in the generated resources.

Lines [65-92]: There are still a few hard-coded user-facing labels ("Grit", "Discord", "Github", etc.). Not urgent, but in a follow-up you might consider moving them into the same resource system for better localization and consistency.

Also applies to: 65-92, 115-115, 133-133

.github/workflows/web.yml (1)

22-31: Consider adding Gradle caching to improve build performance.

Adding Gradle caching will speed up subsequent builds by reusing dependencies and build artifacts.

Apply this diff to add caching:

       - name: Setup Java
         uses: actions/setup-java@v4
         with:
           distribution: temurin
           java-version: 17
+          cache: gradle

       - name: Build Web App
         run: ./gradlew :web:wasmJsBrowserDistribution
shared/core/src/commonMain/composeResources/values/strings.xml (1)

27-27: Consider future plural resource needs.

While the current format string approach works, be aware that some strings (like "best_streak") may need plural forms for proper localization. Based on learnings, migrating to plural resources later can create breaking changes for existing translations.

Consider planning for plural resources in advance:

<!-- Future plural form example -->
<plurals name="best_streak">
    <item quantity="one">Best Streak: %1$d day</item>
    <item quantity="other">Best Streak: %1$d days</item>
</plurals>

Based on learnings.

web/src/jvmMain/kotlin/main.kt (1)

7-16: Consider error handling for DI initialization.

The initKoin() call lacks error handling. If initialization fails (e.g., module configuration errors, duplicate bindings), the app will crash without a meaningful error message.

Additionally, the fixed window size (1000.dp x 600.dp) may not suit all displays. Consider making the window resizable or using a responsive size.

Apply this diff to add error handling:

 fun main() {
-    initKoin()
+    try {
+        initKoin()
+    } catch (e: Exception) {
+        println("Failed to initialize Koin: ${e.message}")
+        throw e
+    }
 
     singleWindowApplication(
         state = WindowState(size = DpSize(1000.dp, 600.dp)),
-        title = "Grit"
+        title = "Grit",
+        resizable = true
     ) {
         App()
     }
 }
app/src/main/java/com/shub39/grit/core/data/NotificationAlarmScheduler.kt (1)

66-74: Replace non-null assertion with safe handling.

Line 69 uses task.reminder!! immediately after a null check on line 68. While this is safe due to the early return, the non-null assertion operator (!!) is fragile and can mask intent.

Apply this diff to use smart cast or safe handling:

 override fun schedule(task: Task) {
     cancel(task)
-    if (task.reminder == null) return
-    val scheduleTime = task.reminder!!
+    val scheduleTime = task.reminder ?: return
 
     if (scheduleTime < Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())) {
         Log.d(tag, "Task '${task.title}' reminder time is in the past")
         return
     }
shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/domain/Category.kt (1)

6-11: Consider the default id = 0 value.

The id field defaults to 0, which could lead to collisions if multiple categories are created without explicitly setting IDs. Ensure that ID assignment is handled properly in the repository layer (e.g., auto-increment or UUID generation).

web/src/commonMain/kotlin/di/initKoin.kt (1)

6-11: Consider guarding against duplicate initialization.

The initKoin function calls startKoin without checking if Koin is already initialized. Calling startKoin multiple times will throw an exception. Consider using startKoin with a check or documenting that this should only be called once.

Apply this diff to make initialization idempotent:

+import org.koin.core.context.GlobalContext
+
 fun initKoin(config: KoinAppDeclaration? = null) {
-    startKoin{
-        config?.invoke(this)
-        modules(appModules)
+    if (GlobalContext.getOrNull() == null) {
+        startKoin {
+            config?.invoke(this)
+            modules(appModules)
+        }
     }
 }
shared/core/src/jvmMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.jvm.kt (1)

29-30: Consider adding a comment explaining the hardcoded permission.

The hardcoded notificationPermission = true and empty onRequestPermission callback are reasonable for JVM targets (desktop/server), but a brief comment would clarify this platform-specific behavior for future maintainers.

Example:

+        // JVM target: notifications are always available or not applicable
         notificationPermission = true,
         onRequestPermission = {},
shared/core/src/wasmJsMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.wasmJs.kt (1)

31-34: Consider adding a comment explaining the hardcoded permission.

Similar to the JVM implementation, hardcoding notificationPermission = true is appropriate for WasmJS (web browser) targets where notifications are handled via the Web Notification API, but a brief comment would improve clarity.

Example:

+        // WasmJS target: notification permissions handled via Web Notification API
         notificationPermission = true,
         showDateTimePicker = showDateTimePicker,
         updateDateTimePickerVisibility = { showDateTimePicker = it },
         onPermissionRequest = {},
shared/core/src/wasmJsMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.wasmJs.kt (1)

29-30: Consider adding a comment explaining the hardcoded permission.

Consistent with other WasmJS implementations, but a brief comment would clarify why notificationPermission is hardcoded to true for web targets.

Example:

+        // WasmJS target: notification permissions handled via Web Notification API
         notificationPermission = true,
         onRequestPermission = {},
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/sections/AnalyticsPage.kt (1)

214-251: Optional: consider localizing dialog contentDescriptions

The visual text for the delete dialog is now properly localized, but contentDescription values like "Warning" remain hard-coded. If you’re aiming for full localization and accessibility parity, consider moving those descriptions to string resources as well.

If you decide to localize them, re-run any accessibility or screen-reader checks you have to confirm the updated descriptions are read correctly.

web/src/commonMain/kotlin/App.kt (2)

62-76: Keep bottom navigation selection in sync with NavHost back stack

currentRoute is tracked with remember { mutableStateOf(Routes.Tasks) } and only updated on bottom-nav clicks. This can drift out of sync with the actual NavHost state (e.g., when the user navigates back), and navController.navigate(currentRoute) will keep pushing new entries onto the stack instead of behaving like typical bottom navigation.

I’d recommend:

  • Deriving the selected route from the navController’s current back stack entry rather than a separate mutable state, and
  • Using navigate(route) { launchSingleTop = true /* plus popUpTo if appropriate */ } for bottom-nav item clicks.

After adjusting, test tapping between Tasks/Habits repeatedly and using back navigation (including browser back on web) to confirm the highlighted tab always matches the visible screen.

Also applies to: 93-115


35-52: Optional: move tab labels to resources for consistency/i18n

Routes.toDisplayText() currently hard-codes "Tasks" and "Habits". Given the rest of the UI is using generated string resources, you may want to move these labels into Res.string.* for localization and consistency.

If you choose to switch to resources, please re-run any localization checks or previews you have to ensure the new strings are wired up correctly.

Also applies to: 84-86

shared/core/src/androidMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.android.kt (1)

51-51: Use string resources for user-facing text.

The toast message "Notification permission denied" is hardcoded. Consider using a string resource for consistency with the rest of the codebase and to support localization.

Apply this diff:

-        } else Toast.makeText(context, "Notification permission denied", Toast.LENGTH_SHORT).show()
+        } else Toast.makeText(context, context.getString(R.string.notification_permission_denied), Toast.LENGTH_SHORT).show()

Note: Ensure the string resource notification_permission_denied is added to the appropriate resources file.

shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/section/EditCategories.kt (1)

52-277: Task categories edit UI migration looks correct; tiny polish possible on count text

The refactor to TaskState / TaskAction and Res.string.* is consistent with the rest of the core/tasks changes, and the reordering/deletion callbacks line up with the TaskAction handlers shown in TasksViewModel.kt. The local categories state derived from state.tasks.keys keeps UI and state aligned during reorders.

One tiny clean-up you might consider:

  • In the support text, prefer keeping the count numeric instead of mixing Int and String:
Text(
    text = "${state.tasks[category]?.size ?: 0} ${stringResource(Res.string.tasks)}"
)

This avoids the "0" string literal and keeps the expression type-safe.

shared/core/src/androidMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.android.kt (1)

19-65: Android HabitUpsertSheet permission flow is solid; consider using a string resource for the toast

The POST_NOTIFICATIONS handling and delegation into HabitUpsertSheetContent are wired correctly and align with Android 13+ patterns. One small improvement:

  • Replace the hard-coded "Notification permission denied" toast text with a shared string resource (e.g., Res.string.notification_permission_denied or an Android-only R.string.*) to keep i18n and wording consistent with the rest of the UI.
Toast.makeText(
    context,
    context.getString(R.string.notification_permission_denied),
    Toast.LENGTH_SHORT
).show()

(or the equivalent via your generated resources on Android).

app/src/main/java/com/shub39/grit/viewmodels/HabitViewModel.kt (1)

131-165: Start-of-week preference currently doesn’t re-drive analytics streams

observeHabitStatuses() and observeOverallAnalytics() capture _state.value.startingDay only once when they’re invoked in the state.onStart block. Later updates from getStartOfTheWeekPref() change startingDay in _state, but don’t re-subscribe the repo flows. If users can change the start-of-week setting at runtime, the analytics/heatmap streams will continue using the original value.

Consider re-invoking observeHabitStatuses() and observeOverallAnalytics() when startingDay changes, for example inside the getStartOfTheWeekPref().onEach block (relying on the existing habitStatusJob/overallAnalyticsJob cancellation):

             datastore
                 .getStartOfTheWeekPref()
-                .onEach { pref ->
-                    _state.update {
-                        it.copy(
-                            startingDay = pref
-                        )
-                    }
-                }
+                .onEach { pref ->
+                    _state.update { it.copy(startingDay = pref) }
+                    observeHabitStatuses()
+                    observeOverallAnalytics()
+                }
                 .launchIn(this)

Also applies to: 167-202

shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/section/TaskList.kt (1)

90-299: Task list state/action migration and reorder logic look consistent

TaskList now correctly consumes TaskState/TaskAction, and all interactions (ChangeCategory, UpsertTask, DeleteTasks, ReorderTasks, AddCategory) dispatch the expected actions; the reorderableTasks local state and ReorderTasks index→task mappings are consistent with the TasksViewModel.onAction implementation. If you want a tiny readability tweak, consider naming the lambda parameter in mapIndexed { newIndex, it -> newIndex to it } (e.g., taskItem) instead of it.

shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/WeeklyGraph.kt (1)

1-107: WeeklyGraph refactor looks solid; consider minor MPP/i18n polish

The migration to HabitsAction, Res.string.*, and List<Line> looks consistent with the core refactor and with HabitViewModel.onAction (including OnShowPaywall). The filtering via takeLast(selectedTimePeriod.toWeeks()) is also safe even when the series is shorter than the requested window.

Two small improvements you might consider:

  • For better localization, instead of string interpolation with "${period.toWeeks()} ${stringResource(Res.string.weeks)}", move this into a formatted string resource (e.g., %d placeholder) so word order and pluralization can be localized properly.
  • Since this composable now lives in commonMain, double‑check that rememberSaveable { mutableStateOf(WeeklyTimePeriod.WEEKS_8) } is supported on all targets. If WeeklyTimePeriod isn’t automatically saveable everywhere, adding an explicit Saver (or falling back to remember) would make this safer in the KMP context.
app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt (2)

31-105: Category selection and delay(200) logic is brittle; consider centralizing in observeTasks

The action handling mostly looks good after the move to TaskAction, but the way currentCategory is managed in the AddCategory, ReorderCategories, and DeleteCategory branches is a bit fragile:

  • Several branches reset currentCategory to it.tasks.keys.firstOrNull() after a delay(200), relying on a timing assumption about when the repository and observeTasks() will have updated state.
  • deleteCategory(category) also updates currentCategory before calling repo.deleteCategory(category), and then the TaskAction.DeleteCategory branch does another update after the artificial delay. That duplication makes it harder to reason about which value “wins” and when.
  • The UX behavior also changed: after Add/ Reorder/ Delete, the selected category always jumps to the first key, which may not match user expectation (e.g., staying on the newly added category, or moving to a neighbor of the deleted one).

It would be more robust to derive currentCategory from the tasks map inside observeTasks() based on the new data (e.g., “if current selection was removed, pick the next/first category”), and avoid magic delays and extra updates inside the action branches. That keeps all category selection rules in one place and decouples them from arbitrary timing.

Also applies to: 136-160


136-138: Localize the default “Misc” category name

addDefault() currently hard-codes the "Misc" label. Given the broader move toward generated Res.string.* usage in this PR, it would be good to move this string into shared resources as well so it can be localized and kept consistent across platforms.

app/build.gradle.kts (1)

13-14: Gradle wiring for shared core and Compose MPP looks reasonable; verify plugin/dependency alignment

The app module’s Gradle changes line up with the KMP refactor:

  • Adding implementation(project(":shared:core")) is the right way to pull in the shared core UI/domain.
  • Centralizing compileSdk, minSdk, and targetSdk via libs.versions.* improves consistency across modules.
  • Switching to compose.* and libs.material3 / libs.navigation.compose matches the move to Compose Multiplatform dependencies.

Given the mix of plugins (android.application, jetbrains.kotlin.android, composeMultiplatform, compose-compiler), it’s worth double‑checking against the current Compose Multiplatform docs that this combination is officially supported for an Android app module (and that the compose-multiplatform and Compose library versions from the catalog are compatible with the Kotlin version you’re using).

Also applies to: 24-30, 124-151

gradle/libs.versions.toml (1)

2-35: Version catalog fits the MPP direction; watch KSP/Kotlin and toolchain compatibility

The catalog updates look aligned with the broader refactor:

  • New entries for compileSdk, minSdk, targetSdk, compose-multiplatform, composeHotReload, Koin, kotlinx-datetime, and coroutines Swing support the shared core and new platform targets.
  • Switching material3 to org.jetbrains.compose.material3:material3 and navigation to org.jetbrains.androidx.navigation:navigation-compose matches the Compose Multiplatform dependency set.

A couple of things to keep an eye on:

  • Kotlin is set to 2.2.21 while KSP is 2.3.0. KSP versions are usually tightly coupled to the Kotlin compiler version; a mismatch here can lead to “incompatible KSP” build failures. It’s safer to keep them in lockstep or confirm that this combo is explicitly supported.
  • The AGP (agp), compose-multiplatform, and Kotlin versions should all be checked together against the current Gradle/Compose compatibility matrix to avoid subtle tooling issues.

Also applies to: 44-45, 54-67, 70-71, 83-83, 85-96

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e4e2d4 and 9798d5e.

⛔ Files ignored due to path filters (1)
  • kotlin-js-store/wasm/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (107)
  • .github/FUNDING.yml (1 hunks)
  • .github/workflows/web.yml (1 hunks)
  • .gitignore (1 hunks)
  • app/build.gradle.kts (3 hunks)
  • app/src/foss/java/com/shub39/grit/billing/PaywallPage.kt (3 hunks)
  • app/src/main/java/com/shub39/grit/app/Grit.kt (5 hunks)
  • app/src/main/java/com/shub39/grit/app/MainActivity.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/data/BootReceiver.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/data/Mappers.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/data/NotificationAlarmScheduler.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/core/data/NotificationReceiver.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/data/backup/Mappers.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/data/backup/export/ExportImpl.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/data/backup/restore/RestoreImpl.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/domain/AlarmScheduler.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/domain/AppTheme.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/component/ColorPickerDialog.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt (3 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/AboutLibraries.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Backup.kt (5 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/LookAndFeelPage.kt (11 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/RootPage.kt (12 hunks)
  • app/src/main/java/com/shub39/grit/core/presentation/util.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/di/GritModules.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/habits/data/repository/HabitRepository.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/habits/data/repository/util.kt (2 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt (0 hunks)
  • app/src/main/java/com/shub39/grit/habits/presentation/HabitsPageAction.kt (0 hunks)
  • app/src/main/java/com/shub39/grit/server/GritServerRepositoryImpl.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/server/domain/CategoryResponse.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/server/domain/TaskResponse.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/tasks/data/repository/TasksRepository.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/TaskPageAction.kt (0 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/TaskPageState.kt (0 hunks)
  • app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (0 hunks)
  • app/src/main/java/com/shub39/grit/viewmodels/HabitViewModel.kt (3 hunks)
  • app/src/main/java/com/shub39/grit/viewmodels/StateLayer.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt (4 hunks)
  • app/src/main/java/com/shub39/grit/widgets/AllTasksWidget.kt (1 hunks)
  • app/src/main/java/com/shub39/grit/widgets/HabitOverviewWidget.kt (4 hunks)
  • app/src/main/java/com/shub39/grit/widgets/HabitStreakWidget.kt (2 hunks)
  • app/src/main/res/values/strings.xml (0 hunks)
  • build.gradle.kts (1 hunks)
  • gradle.properties (1 hunks)
  • gradle/libs.versions.toml (4 hunks)
  • settings.gradle.kts (1 hunks)
  • shared/core/build.gradle.kts (1 hunks)
  • shared/core/src/androidMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.android.kt (1 hunks)
  • shared/core/src/androidMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.android.kt (1 hunks)
  • shared/core/src/androidMain/kotlin/com/shub39/grit/core/utils/blurPossible.android.kt (1 hunks)
  • shared/core/src/commonMain/composeResources/values-es/strings.xml (1 hunks)
  • shared/core/src/commonMain/composeResources/values-uk/strings.xml (1 hunks)
  • shared/core/src/commonMain/composeResources/values/strings.xml (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/Habit.kt (2 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/HabitRepo.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/HabitStatus.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/HabitWithAnalytics.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/OverallAnalytics.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/WeeklyTimePeriod.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/HabitState.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/HabitsAction.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/HabitsGraph.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/AnalyticsCard.kt (4 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/CalendarMap.kt (3 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitCard.kt (5 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitHeatMap.kt (2 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitStartCard.kt (4 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitStreakCard.kt (3 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.kt (12 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/WeekDayBreakdown.kt (3 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/WeeklyActivity.kt (5 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/WeeklyBooleanHeatMap.kt (3 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/WeeklyGraph.kt (4 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/sections/AnalyticsPage.kt (5 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/sections/HabitsList.kt (8 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/sections/OverallAnalytics.kt (4 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/util.kt (3 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/shared_ui/Empty.kt (3 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/shared_ui/GritBottomSheet.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/shared_ui/GritDialog.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/shared_ui/PageFill.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/domain/Category.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/domain/CategoryColors.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/domain/Task.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/domain/TaskRepo.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/TaskAction.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/TaskState.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/TasksGraph.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskCard.kt (2 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.kt (9 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/section/EditCategories.kt (11 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/section/TaskList.kt (14 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/utils/TimeFormats.kt (1 hunks)
  • shared/core/src/commonMain/kotlin/com/shub39/grit/core/utils/blurPossible.kt (1 hunks)
  • shared/core/src/jvmMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.jvm.kt (1 hunks)
  • shared/core/src/jvmMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.jvm.kt (1 hunks)
  • shared/core/src/jvmMain/kotlin/com/shub39/grit/core/utils/blurPossible.jvm.kt (1 hunks)
  • shared/core/src/wasmJsMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.wasmJs.kt (1 hunks)
  • shared/core/src/wasmJsMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.wasmJs.kt (1 hunks)
  • shared/core/src/wasmJsMain/kotlin/com/shub39/grit/core/utils/blurPossible.wasmJs.kt (1 hunks)
  • web/build.gradle.kts (1 hunks)
  • web/src/commonMain/kotlin/App.kt (1 hunks)
  • web/src/commonMain/kotlin/data/DummyStateProvider.kt (1 hunks)
  • web/src/commonMain/kotlin/di/appModules.kt (1 hunks)
  • web/src/commonMain/kotlin/di/initKoin.kt (1 hunks)
  • web/src/commonMain/kotlin/domain/StateProvider.kt (1 hunks)
  • web/src/jvmMain/kotlin/main.kt (1 hunks)
⛔ Files not processed due to max files limit (3)
  • web/src/wasmJsMain/kotlin/main.kt
  • web/src/wasmJsMain/resources/index.html
  • web/webpack.config.d/watch.js
💤 Files with no reviewable changes (6)
  • app/src/main/java/com/shub39/grit/tasks/presentation/TaskPageState.kt
  • app/src/main/java/com/shub39/grit/tasks/presentation/TaskPageAction.kt
  • app/src/main/res/values/strings.xml
  • app/src/main/java/com/shub39/grit/habits/presentation/HabitsGraph.kt
  • app/src/main/java/com/shub39/grit/habits/presentation/HabitsPageAction.kt
  • app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-05T13:36:09.250Z
Learnt from: istudyatuni
Repo: shub39/Grit PR: 103
File: app/src/main/res/values-ru/strings.xml:0-0
Timestamp: 2025-07-05T13:36:09.250Z
Learning: When adding plural resources for Russian strings in Android apps, switching from `stringResource()` to `getQuantityString()` (or `pluralStringResource()` in Compose) creates breaking changes for existing translations. A practical approach is to add commented-out plural forms in the Russian strings file for future migration while keeping the current string resources intact.

Applied to files:

  • shared/core/src/commonMain/composeResources/values-uk/strings.xml
  • app/src/foss/java/com/shub39/grit/billing/PaywallPage.kt
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt
  • app/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Backup.kt
  • shared/core/src/commonMain/composeResources/values-es/strings.xml
  • shared/core/src/commonMain/composeResources/values/strings.xml
🧬 Code graph analysis (24)
shared/core/src/wasmJsMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.wasmJs.kt (1)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.kt (1)
  • TaskUpsertSheetContent (77-285)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/HabitsGraph.kt (3)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/sections/HabitsList.kt (1)
  • HabitsList (69-284)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/sections/AnalyticsPage.kt (1)
  • AnalyticsPage (64-262)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/sections/OverallAnalytics.kt (1)
  • OverallAnalytics (51-170)
shared/core/src/jvmMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.jvm.kt (2)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.kt (1)
  • HabitUpsertSheetContent (70-294)
app/src/main/java/com/shub39/grit/habits/presentation/ui/component/HabitUpsertSheet.kt (1)
  • HabitUpsertSheet (57-291)
web/src/commonMain/kotlin/data/DummyStateProvider.kt (1)
app/src/main/java/com/shub39/grit/habits/data/repository/util.kt (5)
  • prepareLineChartData (97-130)
  • prepareWeekDayFrequencyData (132-144)
  • countCurrentStreak (21-67)
  • countBestStreak (69-94)
  • areConsecutiveEligibleDays (157-171)
shared/core/src/jvmMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.jvm.kt (1)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.kt (1)
  • TaskUpsertSheetContent (77-285)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/WeeklyActivity.kt (1)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/AnalyticsCard.kt (1)
  • AnalyticsCard (28-89)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitHeatMap.kt (1)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/AnalyticsCard.kt (1)
  • AnalyticsCard (28-89)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/WeeklyGraph.kt (2)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/AnalyticsCard.kt (1)
  • AnalyticsCard (28-89)
app/src/main/java/com/shub39/grit/viewmodels/HabitViewModel.kt (1)
  • onAction (55-129)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/AnalyticsCard.kt (4)
shared/core/src/androidMain/kotlin/com/shub39/grit/core/utils/blurPossible.android.kt (1)
  • blurPossible (5-5)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/utils/blurPossible.kt (1)
  • blurPossible (3-3)
shared/core/src/jvmMain/kotlin/com/shub39/grit/core/utils/blurPossible.jvm.kt (1)
  • blurPossible (3-3)
shared/core/src/wasmJsMain/kotlin/com/shub39/grit/core/utils/blurPossible.wasmJs.kt (1)
  • blurPossible (3-3)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/sections/HabitsList.kt (1)
app/src/main/java/com/shub39/grit/viewmodels/HabitViewModel.kt (1)
  • onAction (55-129)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/section/TaskList.kt (1)
app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt (1)
  • onAction (43-107)
web/src/jvmMain/kotlin/main.kt (2)
web/src/commonMain/kotlin/di/initKoin.kt (1)
  • initKoin (6-11)
web/src/commonMain/kotlin/App.kt (1)
  • App (56-117)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/section/EditCategories.kt (1)
app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt (1)
  • onAction (43-107)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/sections/AnalyticsPage.kt (1)
app/src/main/java/com/shub39/grit/viewmodels/HabitViewModel.kt (1)
  • onAction (55-129)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/TasksGraph.kt (3)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/section/TaskList.kt (1)
  • TaskList (87-453)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/section/EditCategories.kt (1)
  • EditCategories (68-283)
app/src/main/java/com/shub39/grit/tasks/presentation/TasksGraph.kt (1)
  • TasksGraph (39-74)
shared/core/src/wasmJsMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.wasmJs.kt (1)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.kt (1)
  • HabitUpsertSheetContent (70-294)
web/src/commonMain/kotlin/App.kt (2)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/TasksGraph.kt (1)
  • TasksGraph (25-60)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/HabitsGraph.kt (1)
  • HabitsGraph (29-69)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/WeeklyBooleanHeatMap.kt (1)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/AnalyticsCard.kt (1)
  • AnalyticsCard (28-89)
app/src/main/java/com/shub39/grit/app/MainActivity.kt (1)
app/src/main/java/com/shub39/grit/app/Grit.kt (1)
  • Grit (70-194)
shared/core/src/androidMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.android.kt (1)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/HabitUpsertSheet.kt (1)
  • HabitUpsertSheetContent (70-294)
shared/core/src/androidMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.android.kt (2)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/tasks/presentation/ui/component/TaskUpsertSheet.kt (1)
  • TaskUpsertSheetContent (77-285)
app/src/main/java/com/shub39/grit/tasks/presentation/ui/component/TaskUpsertSheet.kt (2)
  • TaskUpsertSheet (71-290)
  • {} (272-272)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/WeekDayBreakdown.kt (1)
app/src/main/java/com/shub39/grit/viewmodels/HabitViewModel.kt (1)
  • onAction (55-129)
app/src/main/java/com/shub39/grit/viewmodels/HabitViewModel.kt (2)
app/src/main/java/com/shub39/grit/habits/data/repository/HabitRepository.kt (3)
  • upsertHabit (50-54)
  • deleteHabit (56-60)
  • insertHabitStatus (120-127)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/HabitRepo.kt (4)
  • upsertHabit (7-21)
  • upsertHabit (8-8)
  • deleteHabit (9-9)
  • insertHabitStatus (19-19)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/CalendarMap.kt (2)
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/presentation/ui/component/AnalyticsCard.kt (1)
  • AnalyticsCard (28-89)
app/src/main/java/com/shub39/grit/viewmodels/HabitViewModel.kt (1)
  • onAction (55-129)

Comment on lines +9 to +14
@Serializable
data class OverallAnalytics(
val heatMapData: HeatMapData = emptyMap(),
val weekDayFrequencyData: WeekDayFrequencyData = emptyMap(),
val weeklyGraphData: WeeklyGraphData = emptyMap()
)
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that all nested types are serializable.

The @Serializable annotation requires that all contained types also support serialization. Ensure that Habit, WeeklyComparisonData, and WeekDayFrequencyData are properly annotated with @Serializable or have custom serializers registered.

Run the following script to verify the serialization annotations:


🏁 Script executed:

#!/bin/bash
# Description: Check if the types used in OverallAnalytics have @Serializable annotations

# Search for Habit data class/interface definition
echo "=== Searching for Habit type definition ==="
ast-grep --pattern 'data class Habit'

# Search for WeeklyComparisonData type definition
echo "=== Searching for WeeklyComparisonData type definition ==="
ast-grep --pattern 'data class WeeklyComparisonData'

# Search for WeekDayFrequencyData type alias or definition
echo "=== Searching for WeekDayFrequencyData type ==="
rg -n "WeekDayFrequencyData" --type=kotlin -C3

Length of output: 10298


🏁 Script executed:

#!/bin/bash
# Extended search for missing type definitions

echo "=== Searching for HeatMapData definition ==="
rg -n "HeatMapData\s*=" --type=kotlin -C2

echo "=== Searching for WeeklyGraphData definition ==="
rg -n "WeeklyGraphData\s*=" --type=kotlin -C2

echo "=== Broader search for Habit class (not just data class) ==="
rg -nP "class\s+Habit\b" --type=kotlin -C3

echo "=== Checking OverallAnalytics.kt imports and full context ==="
cat shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/OverallAnalytics.kt

Length of output: 3752


🏁 Script executed:

#!/bin/bash
# Search for custom serializers and Map-related serialization configuration

echo "=== Searching for custom serializers ==="
rg -n "@Serializer\|custom.*[Ss]erializer\|MapSerializer" --type=kotlin -C2

echo "=== Searching for WeeklyGraphData usage in serialization context ==="
rg -n "weeklyGraphData\|WeeklyGraphData" --type=kotlin -C2 | head -50

echo "=== Checking for serializer modules or configurations ==="
rg -n "SerializersModule\|@OptIn.*Serialization" --type=kotlin -C2

Length of output: 167


🏁 Script executed:

#!/bin/bash
# Search for OverallAnalytics actual usage and serialization calls

echo "=== Searching for OverallAnalytics usage ==="
rg -n "OverallAnalytics" --type=kotlin -C2 | head -80

echo "=== Searching for explicit serialization/deserialization calls ==="
rg -n "\.encodeToString\|\.decodeFromString\|Json\.\|json\.encodeToString" --type=kotlin -C1 | head -50

echo "=== Checking build.gradle or similar for kotlinx-serialization version ==="
fd -e "build.gradle" -e "build.gradle.kts" | xargs grep -l "serialization" 2>/dev/null | head -3

Length of output: 8943


Remove @serializable from OverallAnalytics or restructure WeeklyGraphData.

The @Serializable annotation on OverallAnalytics cannot be satisfied because WeeklyGraphData = Map<Habit, WeeklyComparisonData> uses Habit as a Map key. kotlinx.serialization only supports primitive or String types as Map keys; complex types like Habit are not supported, even when marked @Serializable. This will cause a SerializationException at runtime if serialization is attempted.

Options:

  • Remove the @Serializable annotation from OverallAnalytics if serialization is not intended.
  • Restructure WeeklyGraphData to use a serializable key (e.g., Map<Long, WeeklyComparisonData> with habit ID, or a wrapper data class).
🤖 Prompt for AI Agents
In
shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/OverallAnalytics.kt
around lines 9-14, the @Serializable on OverallAnalytics fails because
WeeklyGraphData uses Map<Habit, WeeklyComparisonData> and kotlinx.serialization
only supports primitive/String map keys; either remove @Serializable from
OverallAnalytics if you don't need to serialize it, or change WeeklyGraphData to
use a serializable key (for example Map<Long, WeeklyComparisonData> using habit
IDs or a simple wrapper data class that holds the Habit and is @Serializable)
and update all code that constructs/consumes WeeklyGraphData to map between
Habit and the chosen serializable key.

Comment on lines +50 to +54
compose.desktop {
application {
mainClass = "MainKt"
}
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that MainKt exists in the web module.

The Compose Desktop application is configured with mainClass = "MainKt". Ensure that a Main.kt file with a main() function exists in the web module's source set (likely at web/src/jvmMain/kotlin/Main.kt or web/src/commonMain/kotlin/Main.kt).


🏁 Script executed:

#!/bin/bash
# Check for Main.kt file in web module
fd -e kt "^Main\.kt$" web/src

Length of output: 0


I'll conduct broader searches to verify whether MainKt (the compiled form of Main.kt) exists in the web module.


🏁 Script executed:

#!/bin/bash
# Search more broadly for Main.kt anywhere in the repo
echo "=== Searching for Main.kt in entire repo ==="
fd -e kt "Main\.kt"

echo -e "\n=== Checking web/src directory structure ==="
fd --type d . web/src | head -20

echo -e "\n=== Searching for main function in Kotlin files within web module ==="
rg -n "fun\s+main\s*\(" web/src --type=kt -C2

echo -e "\n=== Listing all .kt files in web/src ==="
fd -e kt . web/src

Length of output: 776


I need to verify the content of those main.kt files and confirm the class name mismatch:


🏁 Script executed:

#!/bin/bash
# Check what's in the jvmMain main.kt file
echo "=== Content of web/src/jvmMain/kotlin/main.kt ==="
cat web/src/jvmMain/kotlin/main.kt

echo -e "\n=== Content of web/src/wasmJsMain/kotlin/main.kt ==="
cat web/src/wasmJsMain/kotlin/main.kt

Length of output: 707


Fix the class name mismatch: rename main.kt to Main.kt or update the Gradle config.

The Gradle configuration references mainClass = "MainKt", but the actual file is named main.kt (lowercase), which Kotlin compiles to the class mainKt (lowercase 'm'). This case mismatch will cause a runtime failure when launching the Compose Desktop application.

Options:

  • Rename web/src/jvmMain/kotlin/main.kt to web/src/jvmMain/kotlin/Main.kt (preferred—matches Gradle expectation)
  • Or change Gradle config to mainClass = "mainKt" (lowercase)
🤖 Prompt for AI Agents
In web/build.gradle.kts around lines 50 to 54, the Gradle setting uses mainClass
= "MainKt" but the source file is named web/src/jvmMain/kotlin/main.kt
(lowercase) causing a class name mismatch at runtime; fix by renaming
web/src/jvmMain/kotlin/main.kt to web/src/jvmMain/kotlin/Main.kt (preferred) so
the compiled class is MainKt, or alternatively change web/build.gradle.kts
mainClass to "mainKt" to match the current filename.

Comment on lines +64 to +67
DynamicMaterialTheme(
primary = Color(0xFABD2F),
isDark = true
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Hex color literal likely yields a fully transparent primary color

DynamicMaterialTheme(primary = Color(0xFABD2F), isDark = true) uses a 6‑digit hex literal. In Compose, Color(0xRRGGBB) is interpreted as 0x00RRGGBB (alpha = 0), so your primary color will be fully transparent instead of an opaque accent.

Consider switching to an 8‑digit ARGB literal, for example:

// Opaque variant of the same RGB
DynamicMaterialTheme(
    primary = Color(0xFFFABD2F),
    isDark = true
)

Please verify visually that the primary color now appears as intended across both Tasks and Habits screens.



---

🏁 Script executed:

```shell
#!/bin/bash
# Inspect the App.kt file at lines 64-67 and surrounding context
fd -e "App.kt" --path "*/commonMain/kotlin/*" --exec head -n 75 {} \; | tail -n 20

Length of output: 233


🌐 Web query:

Jetpack Compose Color hex literal 6-digit vs 8-digit alpha channel

💡 Result:

Short answer

  • Jetpack Compose Color expects a 32‑bit ARGB hex value (0xAARRGGBB). Use 0xFFRRGGBB for an opaque color. [1][2]

Details + examples

  • 0xAARRGGBB — AA = alpha, RR = red, GG = green, BB = blue. Example: Color(0xFF112233) is opaque with R=0x11,G=0x22,B=0x33. [1]
  • To make the same color 50% transparent: Color(0x80112233) (0x80 ≈ 128/255). [1][3]
  • Passing only a 6‑digit literal (e.g. 0x112233) omits the alpha byte and will trigger Compose’s lint warning (“Missing Color alpha channel”) because channels can be undefined — always include the alpha. [2][3]

Sources
[1] AndroidX Compose Color API docs.
[2] Compose ui-graphics lint: Missing Color alpha channel.
[3] Android/Android-hex color explanation and examples.


🏁 Script executed:

#!/bin/bash
# Find App.kt file and display lines 60-75 for context
fd "App.kt" -e kt | while read file; do
  echo "=== File: $file ==="
  sed -n '60,75p' "$file"
done

Length of output: 1344


🏁 Script executed:

#!/bin/bash
# Search for DynamicMaterialTheme usage in kotlin files
rg -n "DynamicMaterialTheme" --type kotlin -A 3 -B 3

Length of output: 2673


Fix hex color literal to include alpha channel for opaque primary color

Color(0xFABD2F) uses a 6-digit hex literal, which omits the alpha channel. Compose Color expects the 0xAARRGGBB format. This will result in undefined behavior or a lint warning. Use Color(0xFFFABD2F) for an opaque variant:

DynamicMaterialTheme(
    primary = Color(0xFFFABD2F),
    isDark = true
)
🤖 Prompt for AI Agents
In web/src/commonMain/kotlin/App.kt around lines 64 to 67 the primary Color
literal uses a 6-digit hex (0xFABD2F) which omits the alpha channel; replace it
with an 8-digit AARRGGBB literal for an opaque color (e.g., 0xFFFABD2F) so
Compose gets a proper 0xAARRGGBB value by updating the Color(...) call
accordingly.

Comment on lines +227 to +353
private fun countCurrentStreak(
dates: List<LocalDate>,
eligibleWeekdays: Set<DayOfWeek> = DayOfWeek.entries.toSet()
): Int {
if (dates.isEmpty()) return 0

val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
val filteredDates = dates.filter { eligibleWeekdays.contains(it.dayOfWeek) }.sorted()

if (filteredDates.isEmpty()) return 0

val lastDate = filteredDates.last()

val daysBetween = lastDate.daysUntil(today)
if (daysBetween > 0) {
var hasEligibleDayMissed = false
for (i in 1..daysBetween) {
val checkDate = lastDate.plus(DatePeriod(days = i))
if (eligibleWeekdays.contains(checkDate.dayOfWeek) && checkDate < today) {
hasEligibleDayMissed = true
break
}
}
if (hasEligibleDayMissed) return 0

if (!eligibleWeekdays.contains(today.dayOfWeek) && daysBetween > 1) {
return 0
}
}

var streak = 1
for (i in filteredDates.size - 2 downTo 0) {
val currentDate = filteredDates[i]
val nextDate = filteredDates[i + 1]

if (areConsecutiveEligibleDays(currentDate, nextDate, eligibleWeekdays)) {
streak++
} else {
break
}
}
return streak
}

private fun countBestStreak(
dates: List<LocalDate>,
eligibleWeekdays: Set<DayOfWeek> = DayOfWeek.entries.toSet()
): Int {
if (dates.isEmpty()) return 0

val filteredDates = dates.filter { eligibleWeekdays.contains(it.dayOfWeek) }.sorted()
if (filteredDates.isEmpty()) return 0

var maxConsecutive = 1
var currentConsecutive = 1

for (i in 1 until filteredDates.size) {
val previousDate = filteredDates[i - 1]
val currentDate = filteredDates[i]

if (areConsecutiveEligibleDays(previousDate, currentDate, eligibleWeekdays)) {
currentConsecutive++
} else {
maxConsecutive = maxOf(maxConsecutive, currentConsecutive)
currentConsecutive = 1
}
}

return maxOf(maxConsecutive, currentConsecutive)
}

private fun prepareLineChartData(
firstDay: DayOfWeek,
habitStatuses: List<HabitStatus>
): WeeklyComparisonData {
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
val totalWeeks = 15

val startDateOfTodayWeek = today.minus(
today.dayOfWeek.isoDayNumber - firstDay.isoDayNumber,
DateTimeUnit.DAY
)
val startDateOfPeriod = startDateOfTodayWeek.minus(totalWeeks, DateTimeUnit.WEEK)

val habitCompletionByWeek = habitStatuses
.filter { it.date in startDateOfPeriod..today }
.groupBy {
val daysFromFirstDay = (it.date.dayOfWeek.isoDayNumber - firstDay.isoDayNumber + 7) % 7
it.date.minus(daysFromFirstDay, DateTimeUnit.DAY)
}
.mapValues { (_, habitStatuses) -> habitStatuses.size }

val values = (0..totalWeeks).map { i ->
val currentWeekStart = startDateOfPeriod.plus(i, DateTimeUnit.WEEK)
(habitCompletionByWeek[currentWeekStart]?.toDouble() ?: 0.0).coerceIn(0.0, 7.0)
}
return values
}

private fun prepareWeekDayFrequencyData(
dates: List<LocalDate>
): WeekDayFrequencyData {
val dayFrequency = dates
.groupingBy { it.dayOfWeek }
.eachCount()

return DayOfWeek.entries.associate {
val weekName = DayOfWeekNames.ENGLISH_ABBREVIATED.names[it.isoDayNumber - 1]

weekName to (dayFrequency[it] ?: 0)
}
}

private fun areConsecutiveEligibleDays(
date1: LocalDate,
date2: LocalDate,
eligibleWeekdays: Set<DayOfWeek>
): Boolean {
var checkDate = date1.plus(1, DateTimeUnit.DAY)
while (checkDate < date2) {
if (eligibleWeekdays.contains(checkDate.dayOfWeek)) {
return false
}
checkDate = checkDate.plus(1, DateTimeUnit.DAY)
}
return checkDate == date2
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Extract duplicated utility functions to shared module.

The functions countCurrentStreak, countBestStreak, prepareLineChartData, prepareWeekDayFrequencyData, and areConsecutiveEligibleDays are duplicated from app/src/main/java/com/shub39/grit/habits/data/repository/util.kt. This creates maintenance burden and increases the risk of inconsistent behavior if bugs are fixed in one location but not the other.

Consider moving these utilities to a shared location such as shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/util/ where both the Android app and web module can access them.

Example structure:

// shared/core/src/commonMain/kotlin/com/shub39/grit/core/habits/domain/util/AnalyticsUtils.kt
package com.shub39.grit.core.habits.domain.util

fun countCurrentStreak(
    dates: List<LocalDate>,
    eligibleWeekdays: Set<DayOfWeek> = DayOfWeek.entries.toSet()
): Int {
    // Implementation
}

fun countBestStreak(
    dates: List<LocalDate>,
    eligibleWeekdays: Set<DayOfWeek> = DayOfWeek.entries.toSet()
): Int {
    // Implementation
}

// ... other utilities

Then import from both modules:

// In DummyStateProvider.kt and Android repository
import com.shub39.grit.core.habits.domain.util.*

This was referenced Nov 17, 2025
@coderabbitai coderabbitai bot mentioned this pull request Dec 2, 2025
Merged
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.

2 participants