-
-
Notifications
You must be signed in to change notification settings - Fork 49
web-ui #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Signed-off-by: shub39 <[email protected]>
Signed-off-by: shub39 <[email protected]>
Signed-off-by: shub39 <[email protected]>
Signed-off-by: shub39 <[email protected]>
Signed-off-by: shub39 <[email protected]>
Signed-off-by: shub39 <[email protected]>
Signed-off-by: shub39 <[email protected]>
Signed-off-by: shub39 <[email protected]>
Code Review AnalysisWalkthroughThis is a major structural refactoring that consolidates domain and presentation layers into a shared Kotlin Multiplatform core module ( Changes
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Key complexity drivers:
Areas requiring special attention during review:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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 shownThe
expect fun TaskUpsertSheetsurface and the split intoTaskUpsertSheetContentwithnotificationPermission/showDateTimePicker/updateDateTimePickerVisibility/onPermissionRequestare reasonable, but the time picker wiring has a functional gap:
showTimePickeris initialized tofalseand never set totrue.- The “Select Time”
IconButtoninsideDatePickerDialogcallsupdateDateTimePickerVisibility(true), which is a no-op if the date-time picker is already visible, and does not flipshowTimePicker.- As a result,
TimePickerDialogis never rendered, so users cannot adjust the reminder time; only the defaultrememberTimePickerStatevalue 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
showDateTimePickerunder 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 datetoday.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 inremember {}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 cleanupLine [7]: Importing
TaskRepofromcom.shub39.grit.core.tasks.domainmatches the new shared core layout and keeps this repository wired to the correct domain interface; behavior is unchanged.Lines [87-89]:
val port = portjust shadows the function parameter and is only used once in the subsequentembeddedServercall. You can drop the extravaland 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 APILines [33-37,115,133]: Using
stringResource(Res.string.bmc)andstringResource(Res.string.rate_on_play)with the generatedResnamespace 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:wasmJsBrowserDistributionshared/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 defaultid = 0value.The
idfield defaults to0, 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
initKoinfunction callsstartKoinwithout checking if Koin is already initialized. CallingstartKoinmultiple times will throw an exception. Consider usingstartKoinwith 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 = trueand emptyonRequestPermissioncallback 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 = trueis 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
notificationPermissionis hardcoded totruefor 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 contentDescriptionsThe visual text for the delete dialog is now properly localized, but
contentDescriptionvalues 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
currentRouteis tracked withremember { 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), andnavController.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 intoRes.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_deniedis 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 textThe refactor to
TaskState/TaskActionandRes.string.*is consistent with the rest of the core/tasks changes, and the reordering/deletion callbacks line up with theTaskActionhandlers shown inTasksViewModel.kt. The localcategoriesstate derived fromstate.tasks.keyskeeps 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
IntandString: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 toastThe POST_NOTIFICATIONS handling and delegation into
HabitUpsertSheetContentare 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_deniedor an Android-onlyR.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()andobserveOverallAnalytics()capture_state.value.startingDayonly once when they’re invoked in thestate.onStartblock. Later updates fromgetStartOfTheWeekPref()changestartingDayin_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()andobserveOverallAnalytics()whenstartingDaychanges, for example inside thegetStartOfTheWeekPref().onEachblock (relying on the existinghabitStatusJob/overallAnalyticsJobcancellation):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
TaskListnow correctly consumesTaskState/TaskAction, and all interactions (ChangeCategory, UpsertTask, DeleteTasks, ReorderTasks, AddCategory) dispatch the expected actions; thereorderableTaskslocal state andReorderTasksindex→task mappings are consistent with theTasksViewModel.onActionimplementation. If you want a tiny readability tweak, consider naming the lambda parameter inmapIndexed { newIndex, it -> newIndex to it }(e.g.,taskItem) instead ofit.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 polishThe migration to
HabitsAction,Res.string.*, andList<Line>looks consistent with the core refactor and withHabitViewModel.onAction(includingOnShowPaywall). The filtering viatakeLast(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.,%dplaceholder) so word order and pluralization can be localized properly.- Since this composable now lives in
commonMain, double‑check thatrememberSaveable { mutableStateOf(WeeklyTimePeriod.WEEKS_8) }is supported on all targets. IfWeeklyTimePeriodisn’t automatically saveable everywhere, adding an explicitSaver(or falling back toremember) would make this safer in the KMP context.app/src/main/java/com/shub39/grit/viewmodels/TasksViewModel.kt (2)
31-105: Category selection anddelay(200)logic is brittle; consider centralizing inobserveTasksThe action handling mostly looks good after the move to
TaskAction, but the waycurrentCategoryis managed in theAddCategory,ReorderCategories, andDeleteCategorybranches is a bit fragile:
- Several branches reset
currentCategorytoit.tasks.keys.firstOrNull()after adelay(200), relying on a timing assumption about when the repository andobserveTasks()will have updated state.deleteCategory(category)also updatescurrentCategorybefore callingrepo.deleteCategory(category), and then theTaskAction.DeleteCategorybranch 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
currentCategoryfrom thetasksmap insideobserveTasks()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 generatedRes.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 alignmentThe 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, andtargetSdkvialibs.versions.*improves consistency across modules.- Switching to
compose.*andlibs.material3/libs.navigation.composematches 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 thecompose-multiplatformand 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 compatibilityThe 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:material3and navigation toorg.jetbrains.androidx.navigation:navigation-composematches the Compose Multiplatform dependency set.A couple of things to keep an eye on:
- Kotlin is set to
2.2.21while KSP is2.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
⛔ Files ignored due to path filters (1)
kotlin-js-store/wasm/yarn.lockis 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.xmlapp/src/foss/java/com/shub39/grit/billing/PaywallPage.ktapp/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.ktapp/src/main/java/com/shub39/grit/core/presentation/settings/ui/section/Backup.ktshared/core/src/commonMain/composeResources/values-es/strings.xmlshared/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)
| @Serializable | ||
| data class OverallAnalytics( | ||
| val heatMapData: HeatMapData = emptyMap(), | ||
| val weekDayFrequencyData: WeekDayFrequencyData = emptyMap(), | ||
| val weeklyGraphData: WeeklyGraphData = emptyMap() | ||
| ) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -C3Length 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.ktLength 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 -C2Length 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 -3Length 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
@Serializableannotation fromOverallAnalyticsif serialization is not intended. - Restructure
WeeklyGraphDatato 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.
| compose.desktop { | ||
| application { | ||
| mainClass = "MainKt" | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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/srcLength 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/srcLength 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.ktLength 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.kttoweb/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.
| DynamicMaterialTheme( | ||
| primary = Color(0xFABD2F), | ||
| isDark = true | ||
| ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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"
doneLength of output: 1344
🏁 Script executed:
#!/bin/bash
# Search for DynamicMaterialTheme usage in kotlin files
rg -n "DynamicMaterialTheme" --type kotlin -A 3 -B 3Length 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.
| 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 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 utilitiesThen import from both modules:
// In DummyStateProvider.kt and Android repository
import com.shub39.grit.core.habits.domain.util.*
Summary by CodeRabbit
New Features
Chores