commit b3bd69ab26cd220c2ab13888a4a02cf893d9b37c Author: whitlocktech Date: Fri May 22 17:21:30 2026 -0500 Initial commit: Implement base health and cycle tracking application. - Set up Android project structure with Gradle 8.7, Kotlin 2.0, and Hilt. - Implement Room database with entities for Profiles, Day Logs, Conditions, Cycle Records, and Intimacy Logs. - Integrate Jetpack Compose for the UI layer including Navigation and Material3. - Develop a cycle prediction engine to calculate menstruation, fertile windows, and ovulation based on user history. - Implement core screens: - **Onboarding:** Initial profile setup for female/male users. - **Calendar:** Monthly view showing cycle phases, logged symptoms, and intimacy records. - **Day Detail:** Detailed logging for symptoms (with ratings), period status, notes, and intimacy encounters. - **Cycle Insights:** Visualization of cycle phases and historical cycle length trends. - **Health Trends:** Frequency and recurrence analysis of logged health conditions over various time ranges. - **Settings:** Profile management, data clearing, and app theme configuration. - Add DataStore for managing user preferences such as active profile and onboarding status. Signed-off-by: whitlocktech diff --git a/.gradle/8.7/checksums/checksums.lock b/.gradle/8.7/checksums/checksums.lock new file mode 100644 index 0000000..85fd161 Binary files /dev/null and b/.gradle/8.7/checksums/checksums.lock differ diff --git a/.gradle/8.7/checksums/md5-checksums.bin b/.gradle/8.7/checksums/md5-checksums.bin new file mode 100644 index 0000000..2f7f21d Binary files /dev/null and b/.gradle/8.7/checksums/md5-checksums.bin differ diff --git a/.gradle/8.7/checksums/sha1-checksums.bin b/.gradle/8.7/checksums/sha1-checksums.bin new file mode 100644 index 0000000..f797fbf Binary files /dev/null and b/.gradle/8.7/checksums/sha1-checksums.bin differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidPluginAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidPluginAccessors.class new file mode 100644 index 0000000..58eb80f Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidPluginAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxActivityLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxActivityLibraryAccessors.class new file mode 100644 index 0000000..ba6e0f1 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxActivityLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxCoreLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxCoreLibraryAccessors.class new file mode 100644 index 0000000..0aa8995 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxCoreLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxLibraryAccessors.class new file mode 100644 index 0000000..16e347b Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$BundleAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$BundleAccessors.class new file mode 100644 index 0000000..b8e1f35 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$BundleAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeLibraryAccessors.class new file mode 100644 index 0000000..866c982 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeMaterialIconsLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeMaterialIconsLibraryAccessors.class new file mode 100644 index 0000000..2f5156f Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeMaterialIconsLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeMaterialLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeMaterialLibraryAccessors.class new file mode 100644 index 0000000..3418aec Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeMaterialLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeUiLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeUiLibraryAccessors.class new file mode 100644 index 0000000..bdbc847 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeUiLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeUiToolingLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeUiToolingLibraryAccessors.class new file mode 100644 index 0000000..5d0e262 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$ComposeUiToolingLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$DatastoreLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$DatastoreLibraryAccessors.class new file mode 100644 index 0000000..17fbad0 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$DatastoreLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$HiltLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$HiltLibraryAccessors.class new file mode 100644 index 0000000..ca41dc8 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$HiltLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$HiltNavigationLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$HiltNavigationLibraryAccessors.class new file mode 100644 index 0000000..2eddac5 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$HiltNavigationLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinPluginAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinPluginAccessors.class new file mode 100644 index 0000000..d1b21e6 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinPluginAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinxCoroutinesLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinxCoroutinesLibraryAccessors.class new file mode 100644 index 0000000..56a777a Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinxCoroutinesLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinxLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinxLibraryAccessors.class new file mode 100644 index 0000000..226b98c Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinxLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinxSerializationLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinxSerializationLibraryAccessors.class new file mode 100644 index 0000000..50bcb45 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$KotlinxSerializationLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$LifecycleLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$LifecycleLibraryAccessors.class new file mode 100644 index 0000000..000a76c Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$LifecycleLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$LifecycleRuntimeLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$LifecycleRuntimeLibraryAccessors.class new file mode 100644 index 0000000..c997146 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$LifecycleRuntimeLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$LifecycleViewmodelLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$LifecycleViewmodelLibraryAccessors.class new file mode 100644 index 0000000..8bd91b1 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$LifecycleViewmodelLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$NavigationLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$NavigationLibraryAccessors.class new file mode 100644 index 0000000..b418a91 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$NavigationLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$PluginAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$PluginAccessors.class new file mode 100644 index 0000000..782e7b2 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$PluginAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$RoomLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$RoomLibraryAccessors.class new file mode 100644 index 0000000..1e7ccaa Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$RoomLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$VersionAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$VersionAccessors.class new file mode 100644 index 0000000..2136c9c Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs$VersionAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs.class new file mode 100644 index 0000000..2defd49 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibs.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidPluginAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidPluginAccessors.class new file mode 100644 index 0000000..d0811e4 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidPluginAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxActivityLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxActivityLibraryAccessors.class new file mode 100644 index 0000000..2248b12 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxActivityLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxCoreLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxCoreLibraryAccessors.class new file mode 100644 index 0000000..7dfb747 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxCoreLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxLibraryAccessors.class new file mode 100644 index 0000000..0f388d5 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$BundleAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$BundleAccessors.class new file mode 100644 index 0000000..cd317e1 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$BundleAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeLibraryAccessors.class new file mode 100644 index 0000000..3f58092 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeMaterialIconsLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeMaterialIconsLibraryAccessors.class new file mode 100644 index 0000000..c16e2a9 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeMaterialIconsLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeMaterialLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeMaterialLibraryAccessors.class new file mode 100644 index 0000000..978cab4 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeMaterialLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeUiLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeUiLibraryAccessors.class new file mode 100644 index 0000000..054ebac Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeUiLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeUiToolingLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeUiToolingLibraryAccessors.class new file mode 100644 index 0000000..a7989f6 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$ComposeUiToolingLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$DatastoreLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$DatastoreLibraryAccessors.class new file mode 100644 index 0000000..878df43 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$DatastoreLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$HiltLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$HiltLibraryAccessors.class new file mode 100644 index 0000000..015df6d Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$HiltLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$HiltNavigationLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$HiltNavigationLibraryAccessors.class new file mode 100644 index 0000000..8a67556 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$HiltNavigationLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinPluginAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinPluginAccessors.class new file mode 100644 index 0000000..64dc5e4 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinPluginAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinxCoroutinesLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinxCoroutinesLibraryAccessors.class new file mode 100644 index 0000000..b840ceb Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinxCoroutinesLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinxLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinxLibraryAccessors.class new file mode 100644 index 0000000..dd61813 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinxLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinxSerializationLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinxSerializationLibraryAccessors.class new file mode 100644 index 0000000..63cc8a1 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$KotlinxSerializationLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$LifecycleLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$LifecycleLibraryAccessors.class new file mode 100644 index 0000000..3d0c160 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$LifecycleLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$LifecycleRuntimeLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$LifecycleRuntimeLibraryAccessors.class new file mode 100644 index 0000000..57d7d64 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$LifecycleRuntimeLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$LifecycleViewmodelLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$LifecycleViewmodelLibraryAccessors.class new file mode 100644 index 0000000..06f8908 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$LifecycleViewmodelLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$NavigationLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$NavigationLibraryAccessors.class new file mode 100644 index 0000000..e6ee481 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$NavigationLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$PluginAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$PluginAccessors.class new file mode 100644 index 0000000..ddddb56 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$PluginAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$RoomLibraryAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$RoomLibraryAccessors.class new file mode 100644 index 0000000..d8c0746 Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$RoomLibraryAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$VersionAccessors.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$VersionAccessors.class new file mode 100644 index 0000000..b56288b Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$VersionAccessors.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.class b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.class new file mode 100644 index 0000000..408636a Binary files /dev/null and b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.class differ diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/metadata.bin b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/metadata.bin new file mode 100644 index 0000000..ad8b584 --- /dev/null +++ b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/metadata.bin @@ -0,0 +1 @@ +›ng6z6wm4ijfn5auy4sgagtfpdeLö¹»u©> b8ƒý + Mîˆclasses‹aÀ:0Ë?¦ý†}ªŒ­¡ˆsources ü PTÕÕײëiŸ›P \ No newline at end of file diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/sources/org/gradle/accessors/dm/LibrariesForLibs.java b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/sources/org/gradle/accessors/dm/LibrariesForLibs.java new file mode 100644 index 0000000..32459fd --- /dev/null +++ b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/sources/org/gradle/accessors/dm/LibrariesForLibs.java @@ -0,0 +1,775 @@ +package org.gradle.accessors.dm; + +import org.gradle.api.NonNullApi; +import org.gradle.api.artifacts.MinimalExternalModuleDependency; +import org.gradle.plugin.use.PluginDependency; +import org.gradle.api.artifacts.ExternalModuleDependencyBundle; +import org.gradle.api.artifacts.MutableVersionConstraint; +import org.gradle.api.provider.Provider; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.internal.catalog.AbstractExternalDependencyFactory; +import org.gradle.api.internal.catalog.DefaultVersionCatalog; +import java.util.Map; +import org.gradle.api.internal.attributes.ImmutableAttributesFactory; +import org.gradle.api.internal.artifacts.dsl.CapabilityNotationParser; +import javax.inject.Inject; + +/** + * A catalog of dependencies accessible via the {@code libs} extension. + */ +@NonNullApi +public class LibrariesForLibs extends AbstractExternalDependencyFactory { + + private final AbstractExternalDependencyFactory owner = this; + private final AndroidxLibraryAccessors laccForAndroidxLibraryAccessors = new AndroidxLibraryAccessors(owner); + private final ComposeLibraryAccessors laccForComposeLibraryAccessors = new ComposeLibraryAccessors(owner); + private final DatastoreLibraryAccessors laccForDatastoreLibraryAccessors = new DatastoreLibraryAccessors(owner); + private final HiltLibraryAccessors laccForHiltLibraryAccessors = new HiltLibraryAccessors(owner); + private final KotlinxLibraryAccessors laccForKotlinxLibraryAccessors = new KotlinxLibraryAccessors(owner); + private final LifecycleLibraryAccessors laccForLifecycleLibraryAccessors = new LifecycleLibraryAccessors(owner); + private final NavigationLibraryAccessors laccForNavigationLibraryAccessors = new NavigationLibraryAccessors(owner); + private final RoomLibraryAccessors laccForRoomLibraryAccessors = new RoomLibraryAccessors(owner); + private final VersionAccessors vaccForVersionAccessors = new VersionAccessors(providers, config); + private final BundleAccessors baccForBundleAccessors = new BundleAccessors(objects, providers, config, attributesFactory, capabilityNotationParser); + private final PluginAccessors paccForPluginAccessors = new PluginAccessors(providers, config); + + @Inject + public LibrariesForLibs(DefaultVersionCatalog config, ProviderFactory providers, ObjectFactory objects, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) { + super(config, providers, objects, attributesFactory, capabilityNotationParser); + } + + /** + * Group of libraries at androidx + */ + public AndroidxLibraryAccessors getAndroidx() { + return laccForAndroidxLibraryAccessors; + } + + /** + * Group of libraries at compose + */ + public ComposeLibraryAccessors getCompose() { + return laccForComposeLibraryAccessors; + } + + /** + * Group of libraries at datastore + */ + public DatastoreLibraryAccessors getDatastore() { + return laccForDatastoreLibraryAccessors; + } + + /** + * Group of libraries at hilt + */ + public HiltLibraryAccessors getHilt() { + return laccForHiltLibraryAccessors; + } + + /** + * Group of libraries at kotlinx + */ + public KotlinxLibraryAccessors getKotlinx() { + return laccForKotlinxLibraryAccessors; + } + + /** + * Group of libraries at lifecycle + */ + public LifecycleLibraryAccessors getLifecycle() { + return laccForLifecycleLibraryAccessors; + } + + /** + * Group of libraries at navigation + */ + public NavigationLibraryAccessors getNavigation() { + return laccForNavigationLibraryAccessors; + } + + /** + * Group of libraries at room + */ + public RoomLibraryAccessors getRoom() { + return laccForRoomLibraryAccessors; + } + + /** + * Group of versions at versions + */ + public VersionAccessors getVersions() { + return vaccForVersionAccessors; + } + + /** + * Group of bundles at bundles + */ + public BundleAccessors getBundles() { + return baccForBundleAccessors; + } + + /** + * Group of plugins at plugins + */ + public PluginAccessors getPlugins() { + return paccForPluginAccessors; + } + + public static class AndroidxLibraryAccessors extends SubDependencyFactory { + private final AndroidxActivityLibraryAccessors laccForAndroidxActivityLibraryAccessors = new AndroidxActivityLibraryAccessors(owner); + private final AndroidxCoreLibraryAccessors laccForAndroidxCoreLibraryAccessors = new AndroidxCoreLibraryAccessors(owner); + + public AndroidxLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Group of libraries at androidx.activity + */ + public AndroidxActivityLibraryAccessors getActivity() { + return laccForAndroidxActivityLibraryAccessors; + } + + /** + * Group of libraries at androidx.core + */ + public AndroidxCoreLibraryAccessors getCore() { + return laccForAndroidxCoreLibraryAccessors; + } + + } + + public static class AndroidxActivityLibraryAccessors extends SubDependencyFactory { + + public AndroidxActivityLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compose with androidx.activity:activity-compose coordinates and + * with version reference activityCompose + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getCompose() { + return create("androidx.activity.compose"); + } + + } + + public static class AndroidxCoreLibraryAccessors extends SubDependencyFactory { + + public AndroidxCoreLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for ktx with androidx.core:core-ktx coordinates and + * with version reference coreKtx + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getKtx() { + return create("androidx.core.ktx"); + } + + } + + public static class ComposeLibraryAccessors extends SubDependencyFactory { + private final ComposeMaterialLibraryAccessors laccForComposeMaterialLibraryAccessors = new ComposeMaterialLibraryAccessors(owner); + private final ComposeUiLibraryAccessors laccForComposeUiLibraryAccessors = new ComposeUiLibraryAccessors(owner); + + public ComposeLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for bom with androidx.compose:compose-bom coordinates and + * with version reference composeBom + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getBom() { + return create("compose.bom"); + } + + /** + * Dependency provider for material3 with androidx.compose.material3:material3 coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getMaterial3() { + return create("compose.material3"); + } + + /** + * Group of libraries at compose.material + */ + public ComposeMaterialLibraryAccessors getMaterial() { + return laccForComposeMaterialLibraryAccessors; + } + + /** + * Group of libraries at compose.ui + */ + public ComposeUiLibraryAccessors getUi() { + return laccForComposeUiLibraryAccessors; + } + + } + + public static class ComposeMaterialLibraryAccessors extends SubDependencyFactory { + private final ComposeMaterialIconsLibraryAccessors laccForComposeMaterialIconsLibraryAccessors = new ComposeMaterialIconsLibraryAccessors(owner); + + public ComposeMaterialLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Group of libraries at compose.material.icons + */ + public ComposeMaterialIconsLibraryAccessors getIcons() { + return laccForComposeMaterialIconsLibraryAccessors; + } + + } + + public static class ComposeMaterialIconsLibraryAccessors extends SubDependencyFactory { + + public ComposeMaterialIconsLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for extended with androidx.compose.material:material-icons-extended coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getExtended() { + return create("compose.material.icons.extended"); + } + + } + + public static class ComposeUiLibraryAccessors extends SubDependencyFactory implements DependencyNotationSupplier { + private final ComposeUiToolingLibraryAccessors laccForComposeUiToolingLibraryAccessors = new ComposeUiToolingLibraryAccessors(owner); + + public ComposeUiLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for ui with androidx.compose.ui:ui coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider asProvider() { + return create("compose.ui"); + } + + /** + * Dependency provider for graphics with androidx.compose.ui:ui-graphics coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getGraphics() { + return create("compose.ui.graphics"); + } + + /** + * Group of libraries at compose.ui.tooling + */ + public ComposeUiToolingLibraryAccessors getTooling() { + return laccForComposeUiToolingLibraryAccessors; + } + + } + + public static class ComposeUiToolingLibraryAccessors extends SubDependencyFactory implements DependencyNotationSupplier { + + public ComposeUiToolingLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for tooling with androidx.compose.ui:ui-tooling coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider asProvider() { + return create("compose.ui.tooling"); + } + + /** + * Dependency provider for preview with androidx.compose.ui:ui-tooling-preview coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getPreview() { + return create("compose.ui.tooling.preview"); + } + + } + + public static class DatastoreLibraryAccessors extends SubDependencyFactory { + + public DatastoreLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for preferences with androidx.datastore:datastore-preferences coordinates and + * with version reference datastore + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getPreferences() { + return create("datastore.preferences"); + } + + } + + public static class HiltLibraryAccessors extends SubDependencyFactory { + private final HiltNavigationLibraryAccessors laccForHiltNavigationLibraryAccessors = new HiltNavigationLibraryAccessors(owner); + + public HiltLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for android with com.google.dagger:hilt-android coordinates and + * with version reference hilt + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getAndroid() { + return create("hilt.android"); + } + + /** + * Dependency provider for compiler with com.google.dagger:hilt-android-compiler coordinates and + * with version reference hilt + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getCompiler() { + return create("hilt.compiler"); + } + + /** + * Group of libraries at hilt.navigation + */ + public HiltNavigationLibraryAccessors getNavigation() { + return laccForHiltNavigationLibraryAccessors; + } + + } + + public static class HiltNavigationLibraryAccessors extends SubDependencyFactory { + + public HiltNavigationLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compose with androidx.hilt:hilt-navigation-compose coordinates and + * with version reference hiltNavigationCompose + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getCompose() { + return create("hilt.navigation.compose"); + } + + } + + public static class KotlinxLibraryAccessors extends SubDependencyFactory { + private final KotlinxCoroutinesLibraryAccessors laccForKotlinxCoroutinesLibraryAccessors = new KotlinxCoroutinesLibraryAccessors(owner); + private final KotlinxSerializationLibraryAccessors laccForKotlinxSerializationLibraryAccessors = new KotlinxSerializationLibraryAccessors(owner); + + public KotlinxLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Group of libraries at kotlinx.coroutines + */ + public KotlinxCoroutinesLibraryAccessors getCoroutines() { + return laccForKotlinxCoroutinesLibraryAccessors; + } + + /** + * Group of libraries at kotlinx.serialization + */ + public KotlinxSerializationLibraryAccessors getSerialization() { + return laccForKotlinxSerializationLibraryAccessors; + } + + } + + public static class KotlinxCoroutinesLibraryAccessors extends SubDependencyFactory { + + public KotlinxCoroutinesLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for android with org.jetbrains.kotlinx:kotlinx-coroutines-android coordinates and + * with version reference coroutines + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getAndroid() { + return create("kotlinx.coroutines.android"); + } + + } + + public static class KotlinxSerializationLibraryAccessors extends SubDependencyFactory { + + public KotlinxSerializationLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for json with org.jetbrains.kotlinx:kotlinx-serialization-json coordinates and + * with version reference serialization + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getJson() { + return create("kotlinx.serialization.json"); + } + + } + + public static class LifecycleLibraryAccessors extends SubDependencyFactory { + private final LifecycleRuntimeLibraryAccessors laccForLifecycleRuntimeLibraryAccessors = new LifecycleRuntimeLibraryAccessors(owner); + private final LifecycleViewmodelLibraryAccessors laccForLifecycleViewmodelLibraryAccessors = new LifecycleViewmodelLibraryAccessors(owner); + + public LifecycleLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Group of libraries at lifecycle.runtime + */ + public LifecycleRuntimeLibraryAccessors getRuntime() { + return laccForLifecycleRuntimeLibraryAccessors; + } + + /** + * Group of libraries at lifecycle.viewmodel + */ + public LifecycleViewmodelLibraryAccessors getViewmodel() { + return laccForLifecycleViewmodelLibraryAccessors; + } + + } + + public static class LifecycleRuntimeLibraryAccessors extends SubDependencyFactory { + + public LifecycleRuntimeLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compose with androidx.lifecycle:lifecycle-runtime-compose coordinates and + * with version reference lifecycle + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getCompose() { + return create("lifecycle.runtime.compose"); + } + + /** + * Dependency provider for ktx with androidx.lifecycle:lifecycle-runtime-ktx coordinates and + * with version reference lifecycle + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getKtx() { + return create("lifecycle.runtime.ktx"); + } + + } + + public static class LifecycleViewmodelLibraryAccessors extends SubDependencyFactory { + + public LifecycleViewmodelLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compose with androidx.lifecycle:lifecycle-viewmodel-compose coordinates and + * with version reference lifecycle + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getCompose() { + return create("lifecycle.viewmodel.compose"); + } + + } + + public static class NavigationLibraryAccessors extends SubDependencyFactory { + + public NavigationLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compose with androidx.navigation:navigation-compose coordinates and + * with version reference navigationCompose + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getCompose() { + return create("navigation.compose"); + } + + } + + public static class RoomLibraryAccessors extends SubDependencyFactory { + + public RoomLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compiler with androidx.room:room-compiler coordinates and + * with version reference room + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getCompiler() { + return create("room.compiler"); + } + + /** + * Dependency provider for ktx with androidx.room:room-ktx coordinates and + * with version reference room + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getKtx() { + return create("room.ktx"); + } + + /** + * Dependency provider for runtime with androidx.room:room-runtime coordinates and + * with version reference room + *

+ * This dependency was declared in catalog libs.versions.toml + */ + public Provider getRuntime() { + return create("room.runtime"); + } + + } + + public static class VersionAccessors extends VersionFactory { + + public VersionAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); } + + /** + * Version alias activityCompose with value 1.9.0 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getActivityCompose() { return getVersion("activityCompose"); } + + /** + * Version alias agp with value 8.4.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getAgp() { return getVersion("agp"); } + + /** + * Version alias composeBom with value 2024.06.00 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getComposeBom() { return getVersion("composeBom"); } + + /** + * Version alias coreKtx with value 1.13.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getCoreKtx() { return getVersion("coreKtx"); } + + /** + * Version alias coroutines with value 1.8.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getCoroutines() { return getVersion("coroutines"); } + + /** + * Version alias datastore with value 1.1.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getDatastore() { return getVersion("datastore"); } + + /** + * Version alias hilt with value 2.51.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getHilt() { return getVersion("hilt"); } + + /** + * Version alias hiltNavigationCompose with value 1.2.0 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getHiltNavigationCompose() { return getVersion("hiltNavigationCompose"); } + + /** + * Version alias kotlin with value 2.0.0 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getKotlin() { return getVersion("kotlin"); } + + /** + * Version alias ksp with value 2.0.0-1.0.21 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getKsp() { return getVersion("ksp"); } + + /** + * Version alias lifecycle with value 2.8.2 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getLifecycle() { return getVersion("lifecycle"); } + + /** + * Version alias navigationCompose with value 2.7.7 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getNavigationCompose() { return getVersion("navigationCompose"); } + + /** + * Version alias room with value 2.6.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getRoom() { return getVersion("room"); } + + /** + * Version alias serialization with value 1.7.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getSerialization() { return getVersion("serialization"); } + + } + + public static class BundleAccessors extends BundleFactory { + + public BundleAccessors(ObjectFactory objects, ProviderFactory providers, DefaultVersionCatalog config, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) { super(objects, providers, config, attributesFactory, capabilityNotationParser); } + + } + + public static class PluginAccessors extends PluginFactory { + private final AndroidPluginAccessors paccForAndroidPluginAccessors = new AndroidPluginAccessors(providers, config); + private final KotlinPluginAccessors paccForKotlinPluginAccessors = new KotlinPluginAccessors(providers, config); + + public PluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); } + + /** + * Plugin provider for hilt with plugin id com.google.dagger.hilt.android and + * with version reference hilt + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getHilt() { return createPlugin("hilt"); } + + /** + * Plugin provider for ksp with plugin id com.google.devtools.ksp and + * with version reference ksp + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getKsp() { return createPlugin("ksp"); } + + /** + * Group of plugins at plugins.android + */ + public AndroidPluginAccessors getAndroid() { + return paccForAndroidPluginAccessors; + } + + /** + * Group of plugins at plugins.kotlin + */ + public KotlinPluginAccessors getKotlin() { + return paccForKotlinPluginAccessors; + } + + } + + public static class AndroidPluginAccessors extends PluginFactory { + + public AndroidPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); } + + /** + * Plugin provider for android.application with plugin id com.android.application and + * with version reference agp + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getApplication() { return createPlugin("android.application"); } + + } + + public static class KotlinPluginAccessors extends PluginFactory { + + public KotlinPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); } + + /** + * Plugin provider for kotlin.android with plugin id org.jetbrains.kotlin.android and + * with version reference kotlin + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getAndroid() { return createPlugin("kotlin.android"); } + + /** + * Plugin provider for kotlin.compose with plugin id org.jetbrains.kotlin.plugin.compose and + * with version reference kotlin + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getCompose() { return createPlugin("kotlin.compose"); } + + /** + * Plugin provider for kotlin.serialization with plugin id org.jetbrains.kotlin.plugin.serialization and + * with version reference kotlin + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getSerialization() { return createPlugin("kotlin.serialization"); } + + } + +} diff --git a/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/sources/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.java b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/sources/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.java new file mode 100644 index 0000000..7f4717c --- /dev/null +++ b/.gradle/8.7/dependencies-accessors/4d36d099e6cbda31bf86da5ce95a8407329b9417/sources/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.java @@ -0,0 +1,1023 @@ +package org.gradle.accessors.dm; + +import org.gradle.api.NonNullApi; +import org.gradle.api.artifacts.MinimalExternalModuleDependency; +import org.gradle.plugin.use.PluginDependency; +import org.gradle.api.artifacts.ExternalModuleDependencyBundle; +import org.gradle.api.artifacts.MutableVersionConstraint; +import org.gradle.api.provider.Provider; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.internal.catalog.AbstractExternalDependencyFactory; +import org.gradle.api.internal.catalog.DefaultVersionCatalog; +import java.util.Map; +import org.gradle.api.internal.attributes.ImmutableAttributesFactory; +import org.gradle.api.internal.artifacts.dsl.CapabilityNotationParser; +import javax.inject.Inject; + +/** + * A catalog of dependencies accessible via the {@code libs} extension. + */ +@NonNullApi +public class LibrariesForLibsInPluginsBlock extends AbstractExternalDependencyFactory { + + private final AbstractExternalDependencyFactory owner = this; + private final AndroidxLibraryAccessors laccForAndroidxLibraryAccessors = new AndroidxLibraryAccessors(owner); + private final ComposeLibraryAccessors laccForComposeLibraryAccessors = new ComposeLibraryAccessors(owner); + private final DatastoreLibraryAccessors laccForDatastoreLibraryAccessors = new DatastoreLibraryAccessors(owner); + private final HiltLibraryAccessors laccForHiltLibraryAccessors = new HiltLibraryAccessors(owner); + private final KotlinxLibraryAccessors laccForKotlinxLibraryAccessors = new KotlinxLibraryAccessors(owner); + private final LifecycleLibraryAccessors laccForLifecycleLibraryAccessors = new LifecycleLibraryAccessors(owner); + private final NavigationLibraryAccessors laccForNavigationLibraryAccessors = new NavigationLibraryAccessors(owner); + private final RoomLibraryAccessors laccForRoomLibraryAccessors = new RoomLibraryAccessors(owner); + private final VersionAccessors vaccForVersionAccessors = new VersionAccessors(providers, config); + private final BundleAccessors baccForBundleAccessors = new BundleAccessors(objects, providers, config, attributesFactory, capabilityNotationParser); + private final PluginAccessors paccForPluginAccessors = new PluginAccessors(providers, config); + + @Inject + public LibrariesForLibsInPluginsBlock(DefaultVersionCatalog config, ProviderFactory providers, ObjectFactory objects, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) { + super(config, providers, objects, attributesFactory, capabilityNotationParser); + } + + /** + * Group of libraries at androidx + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public AndroidxLibraryAccessors getAndroidx() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForAndroidxLibraryAccessors; + } + + /** + * Group of libraries at compose + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public ComposeLibraryAccessors getCompose() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForComposeLibraryAccessors; + } + + /** + * Group of libraries at datastore + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public DatastoreLibraryAccessors getDatastore() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForDatastoreLibraryAccessors; + } + + /** + * Group of libraries at hilt + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public HiltLibraryAccessors getHilt() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForHiltLibraryAccessors; + } + + /** + * Group of libraries at kotlinx + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public KotlinxLibraryAccessors getKotlinx() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForKotlinxLibraryAccessors; + } + + /** + * Group of libraries at lifecycle + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public LifecycleLibraryAccessors getLifecycle() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForLifecycleLibraryAccessors; + } + + /** + * Group of libraries at navigation + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public NavigationLibraryAccessors getNavigation() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForNavigationLibraryAccessors; + } + + /** + * Group of libraries at room + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public RoomLibraryAccessors getRoom() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForRoomLibraryAccessors; + } + + /** + * Group of versions at versions + */ + public VersionAccessors getVersions() { + return vaccForVersionAccessors; + } + + /** + * Group of bundles at bundles + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public BundleAccessors getBundles() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return baccForBundleAccessors; + } + + /** + * Group of plugins at plugins + */ + public PluginAccessors getPlugins() { + return paccForPluginAccessors; + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class AndroidxLibraryAccessors extends SubDependencyFactory { + private final AndroidxActivityLibraryAccessors laccForAndroidxActivityLibraryAccessors = new AndroidxActivityLibraryAccessors(owner); + private final AndroidxCoreLibraryAccessors laccForAndroidxCoreLibraryAccessors = new AndroidxCoreLibraryAccessors(owner); + + public AndroidxLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Group of libraries at androidx.activity + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public AndroidxActivityLibraryAccessors getActivity() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForAndroidxActivityLibraryAccessors; + } + + /** + * Group of libraries at androidx.core + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public AndroidxCoreLibraryAccessors getCore() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForAndroidxCoreLibraryAccessors; + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class AndroidxActivityLibraryAccessors extends SubDependencyFactory { + + public AndroidxActivityLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compose with androidx.activity:activity-compose coordinates and + * with version reference activityCompose + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getCompose() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("androidx.activity.compose"); + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class AndroidxCoreLibraryAccessors extends SubDependencyFactory { + + public AndroidxCoreLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for ktx with androidx.core:core-ktx coordinates and + * with version reference coreKtx + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getKtx() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("androidx.core.ktx"); + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class ComposeLibraryAccessors extends SubDependencyFactory { + private final ComposeMaterialLibraryAccessors laccForComposeMaterialLibraryAccessors = new ComposeMaterialLibraryAccessors(owner); + private final ComposeUiLibraryAccessors laccForComposeUiLibraryAccessors = new ComposeUiLibraryAccessors(owner); + + public ComposeLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for bom with androidx.compose:compose-bom coordinates and + * with version reference composeBom + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getBom() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("compose.bom"); + } + + /** + * Dependency provider for material3 with androidx.compose.material3:material3 coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getMaterial3() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("compose.material3"); + } + + /** + * Group of libraries at compose.material + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public ComposeMaterialLibraryAccessors getMaterial() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForComposeMaterialLibraryAccessors; + } + + /** + * Group of libraries at compose.ui + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public ComposeUiLibraryAccessors getUi() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForComposeUiLibraryAccessors; + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class ComposeMaterialLibraryAccessors extends SubDependencyFactory { + private final ComposeMaterialIconsLibraryAccessors laccForComposeMaterialIconsLibraryAccessors = new ComposeMaterialIconsLibraryAccessors(owner); + + public ComposeMaterialLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Group of libraries at compose.material.icons + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public ComposeMaterialIconsLibraryAccessors getIcons() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForComposeMaterialIconsLibraryAccessors; + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class ComposeMaterialIconsLibraryAccessors extends SubDependencyFactory { + + public ComposeMaterialIconsLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for extended with androidx.compose.material:material-icons-extended coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getExtended() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("compose.material.icons.extended"); + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class ComposeUiLibraryAccessors extends SubDependencyFactory implements DependencyNotationSupplier { + private final ComposeUiToolingLibraryAccessors laccForComposeUiToolingLibraryAccessors = new ComposeUiToolingLibraryAccessors(owner); + + public ComposeUiLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for ui with androidx.compose.ui:ui coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider asProvider() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("compose.ui"); + } + + /** + * Dependency provider for graphics with androidx.compose.ui:ui-graphics coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getGraphics() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("compose.ui.graphics"); + } + + /** + * Group of libraries at compose.ui.tooling + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public ComposeUiToolingLibraryAccessors getTooling() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForComposeUiToolingLibraryAccessors; + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class ComposeUiToolingLibraryAccessors extends SubDependencyFactory implements DependencyNotationSupplier { + + public ComposeUiToolingLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for tooling with androidx.compose.ui:ui-tooling coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider asProvider() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("compose.ui.tooling"); + } + + /** + * Dependency provider for preview with androidx.compose.ui:ui-tooling-preview coordinates and + * with no version specified + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getPreview() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("compose.ui.tooling.preview"); + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class DatastoreLibraryAccessors extends SubDependencyFactory { + + public DatastoreLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for preferences with androidx.datastore:datastore-preferences coordinates and + * with version reference datastore + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getPreferences() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("datastore.preferences"); + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class HiltLibraryAccessors extends SubDependencyFactory { + private final HiltNavigationLibraryAccessors laccForHiltNavigationLibraryAccessors = new HiltNavigationLibraryAccessors(owner); + + public HiltLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for android with com.google.dagger:hilt-android coordinates and + * with version reference hilt + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getAndroid() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("hilt.android"); + } + + /** + * Dependency provider for compiler with com.google.dagger:hilt-android-compiler coordinates and + * with version reference hilt + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getCompiler() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("hilt.compiler"); + } + + /** + * Group of libraries at hilt.navigation + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public HiltNavigationLibraryAccessors getNavigation() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForHiltNavigationLibraryAccessors; + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class HiltNavigationLibraryAccessors extends SubDependencyFactory { + + public HiltNavigationLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compose with androidx.hilt:hilt-navigation-compose coordinates and + * with version reference hiltNavigationCompose + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getCompose() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("hilt.navigation.compose"); + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class KotlinxLibraryAccessors extends SubDependencyFactory { + private final KotlinxCoroutinesLibraryAccessors laccForKotlinxCoroutinesLibraryAccessors = new KotlinxCoroutinesLibraryAccessors(owner); + private final KotlinxSerializationLibraryAccessors laccForKotlinxSerializationLibraryAccessors = new KotlinxSerializationLibraryAccessors(owner); + + public KotlinxLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Group of libraries at kotlinx.coroutines + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public KotlinxCoroutinesLibraryAccessors getCoroutines() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForKotlinxCoroutinesLibraryAccessors; + } + + /** + * Group of libraries at kotlinx.serialization + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public KotlinxSerializationLibraryAccessors getSerialization() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForKotlinxSerializationLibraryAccessors; + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class KotlinxCoroutinesLibraryAccessors extends SubDependencyFactory { + + public KotlinxCoroutinesLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for android with org.jetbrains.kotlinx:kotlinx-coroutines-android coordinates and + * with version reference coroutines + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getAndroid() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("kotlinx.coroutines.android"); + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class KotlinxSerializationLibraryAccessors extends SubDependencyFactory { + + public KotlinxSerializationLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for json with org.jetbrains.kotlinx:kotlinx-serialization-json coordinates and + * with version reference serialization + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getJson() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("kotlinx.serialization.json"); + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class LifecycleLibraryAccessors extends SubDependencyFactory { + private final LifecycleRuntimeLibraryAccessors laccForLifecycleRuntimeLibraryAccessors = new LifecycleRuntimeLibraryAccessors(owner); + private final LifecycleViewmodelLibraryAccessors laccForLifecycleViewmodelLibraryAccessors = new LifecycleViewmodelLibraryAccessors(owner); + + public LifecycleLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Group of libraries at lifecycle.runtime + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public LifecycleRuntimeLibraryAccessors getRuntime() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForLifecycleRuntimeLibraryAccessors; + } + + /** + * Group of libraries at lifecycle.viewmodel + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public LifecycleViewmodelLibraryAccessors getViewmodel() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return laccForLifecycleViewmodelLibraryAccessors; + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class LifecycleRuntimeLibraryAccessors extends SubDependencyFactory { + + public LifecycleRuntimeLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compose with androidx.lifecycle:lifecycle-runtime-compose coordinates and + * with version reference lifecycle + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getCompose() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("lifecycle.runtime.compose"); + } + + /** + * Dependency provider for ktx with androidx.lifecycle:lifecycle-runtime-ktx coordinates and + * with version reference lifecycle + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getKtx() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("lifecycle.runtime.ktx"); + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class LifecycleViewmodelLibraryAccessors extends SubDependencyFactory { + + public LifecycleViewmodelLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compose with androidx.lifecycle:lifecycle-viewmodel-compose coordinates and + * with version reference lifecycle + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getCompose() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("lifecycle.viewmodel.compose"); + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class NavigationLibraryAccessors extends SubDependencyFactory { + + public NavigationLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compose with androidx.navigation:navigation-compose coordinates and + * with version reference navigationCompose + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getCompose() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("navigation.compose"); + } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class RoomLibraryAccessors extends SubDependencyFactory { + + public RoomLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); } + + /** + * Dependency provider for compiler with androidx.room:room-compiler coordinates and + * with version reference room + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getCompiler() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("room.compiler"); + } + + /** + * Dependency provider for ktx with androidx.room:room-ktx coordinates and + * with version reference room + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getKtx() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("room.ktx"); + } + + /** + * Dependency provider for runtime with androidx.room:room-runtime coordinates and + * with version reference room + *

+ * This dependency was declared in catalog libs.versions.toml + * + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public Provider getRuntime() { + org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser(); + return create("room.runtime"); + } + + } + + public static class VersionAccessors extends VersionFactory { + + public VersionAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); } + + /** + * Version alias activityCompose with value 1.9.0 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getActivityCompose() { return getVersion("activityCompose"); } + + /** + * Version alias agp with value 8.4.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getAgp() { return getVersion("agp"); } + + /** + * Version alias composeBom with value 2024.06.00 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getComposeBom() { return getVersion("composeBom"); } + + /** + * Version alias coreKtx with value 1.13.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getCoreKtx() { return getVersion("coreKtx"); } + + /** + * Version alias coroutines with value 1.8.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getCoroutines() { return getVersion("coroutines"); } + + /** + * Version alias datastore with value 1.1.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getDatastore() { return getVersion("datastore"); } + + /** + * Version alias hilt with value 2.51.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getHilt() { return getVersion("hilt"); } + + /** + * Version alias hiltNavigationCompose with value 1.2.0 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getHiltNavigationCompose() { return getVersion("hiltNavigationCompose"); } + + /** + * Version alias kotlin with value 2.0.0 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getKotlin() { return getVersion("kotlin"); } + + /** + * Version alias ksp with value 2.0.0-1.0.21 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getKsp() { return getVersion("ksp"); } + + /** + * Version alias lifecycle with value 2.8.2 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getLifecycle() { return getVersion("lifecycle"); } + + /** + * Version alias navigationCompose with value 2.7.7 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getNavigationCompose() { return getVersion("navigationCompose"); } + + /** + * Version alias room with value 2.6.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getRoom() { return getVersion("room"); } + + /** + * Version alias serialization with value 1.7.1 + *

+ * If the version is a rich version and cannot be represented as a + * single version string, an empty string is returned. + *

+ * This version was declared in catalog libs.versions.toml + */ + public Provider getSerialization() { return getVersion("serialization"); } + + } + + /** + * @deprecated Will be removed in Gradle 9.0. + */ + @Deprecated + public static class BundleAccessors extends BundleFactory { + + public BundleAccessors(ObjectFactory objects, ProviderFactory providers, DefaultVersionCatalog config, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) { super(objects, providers, config, attributesFactory, capabilityNotationParser); } + + } + + public static class PluginAccessors extends PluginFactory { + private final AndroidPluginAccessors paccForAndroidPluginAccessors = new AndroidPluginAccessors(providers, config); + private final KotlinPluginAccessors paccForKotlinPluginAccessors = new KotlinPluginAccessors(providers, config); + + public PluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); } + + /** + * Plugin provider for hilt with plugin id com.google.dagger.hilt.android and + * with version reference hilt + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getHilt() { return createPlugin("hilt"); } + + /** + * Plugin provider for ksp with plugin id com.google.devtools.ksp and + * with version reference ksp + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getKsp() { return createPlugin("ksp"); } + + /** + * Group of plugins at plugins.android + */ + public AndroidPluginAccessors getAndroid() { + return paccForAndroidPluginAccessors; + } + + /** + * Group of plugins at plugins.kotlin + */ + public KotlinPluginAccessors getKotlin() { + return paccForKotlinPluginAccessors; + } + + } + + public static class AndroidPluginAccessors extends PluginFactory { + + public AndroidPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); } + + /** + * Plugin provider for android.application with plugin id com.android.application and + * with version reference agp + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getApplication() { return createPlugin("android.application"); } + + } + + public static class KotlinPluginAccessors extends PluginFactory { + + public KotlinPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); } + + /** + * Plugin provider for kotlin.android with plugin id org.jetbrains.kotlin.android and + * with version reference kotlin + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getAndroid() { return createPlugin("kotlin.android"); } + + /** + * Plugin provider for kotlin.compose with plugin id org.jetbrains.kotlin.plugin.compose and + * with version reference kotlin + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getCompose() { return createPlugin("kotlin.compose"); } + + /** + * Plugin provider for kotlin.serialization with plugin id org.jetbrains.kotlin.plugin.serialization and + * with version reference kotlin + *

+ * This plugin was declared in catalog libs.versions.toml + */ + public Provider getSerialization() { return createPlugin("kotlin.serialization"); } + + } + +} diff --git a/.gradle/8.7/executionHistory/executionHistory.lock b/.gradle/8.7/executionHistory/executionHistory.lock new file mode 100644 index 0000000..b2dd250 Binary files /dev/null and b/.gradle/8.7/executionHistory/executionHistory.lock differ diff --git a/.gradle/8.7/fileHashes/fileHashes.bin b/.gradle/8.7/fileHashes/fileHashes.bin new file mode 100644 index 0000000..cd83fa9 Binary files /dev/null and b/.gradle/8.7/fileHashes/fileHashes.bin differ diff --git a/.gradle/8.7/fileHashes/fileHashes.lock b/.gradle/8.7/fileHashes/fileHashes.lock new file mode 100644 index 0000000..bb530a7 Binary files /dev/null and b/.gradle/8.7/fileHashes/fileHashes.lock differ diff --git a/.gradle/8.7/fileHashes/resourceHashesCache.bin b/.gradle/8.7/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000..4478e4d Binary files /dev/null and b/.gradle/8.7/fileHashes/resourceHashesCache.bin differ diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..72fbd61 Binary files /dev/null and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..0ae8178 --- /dev/null +++ b/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Fri May 22 16:56:02 CDT 2026 +gradle.version=8.7 diff --git a/.gradle/config.properties b/.gradle/config.properties new file mode 100644 index 0000000..16d1193 --- /dev/null +++ b/.gradle/config.properties @@ -0,0 +1,2 @@ +#Fri May 22 16:55:12 CDT 2026 +java.home=C\:\\Program Files\\Android\\Android Studio\\jbr diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe new file mode 100644 index 0000000..1333b3d Binary files /dev/null and b/.gradle/file-system.probe differ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..abd93fc --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +HSDiary \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml new file mode 100644 index 0000000..fadb0d1 --- /dev/null +++ b/.idea/caches/deviceStreaming.xml @@ -0,0 +1,2153 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..f299359 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..97f0a8e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..d0752fa --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,76 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.hilt) + alias(libs.plugins.ksp) +} + +android { + namespace = "com.hsdiary" + compileSdk = 34 + + defaultConfig { + applicationId = "com.hsdiary" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + compose = true + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.ui.graphics) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.material3) + implementation(libs.compose.material.icons.extended) + implementation(libs.lifecycle.runtime.ktx) + implementation(libs.lifecycle.viewmodel.compose) + implementation(libs.lifecycle.runtime.compose) + implementation(libs.navigation.compose) + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + implementation(libs.hilt.navigation.compose) + implementation(libs.room.runtime) + implementation(libs.room.ktx) + ksp(libs.room.compiler) + implementation(libs.datastore.preferences) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.serialization.json) + debugImplementation(libs.compose.ui.tooling) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..215a79c --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,2 @@ +-keep class com.hsdiary.** { *; } +-keepattributes *Annotation* diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2ba417e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/app/src/main/java/com/hsdiary/HSDiaryApplication.kt b/app/src/main/java/com/hsdiary/HSDiaryApplication.kt new file mode 100644 index 0000000..01c311f --- /dev/null +++ b/app/src/main/java/com/hsdiary/HSDiaryApplication.kt @@ -0,0 +1,7 @@ +package com.hsdiary + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class HSDiaryApplication : Application() diff --git a/app/src/main/java/com/hsdiary/MainActivity.kt b/app/src/main/java/com/hsdiary/MainActivity.kt new file mode 100644 index 0000000..9c19d4c --- /dev/null +++ b/app/src/main/java/com/hsdiary/MainActivity.kt @@ -0,0 +1,22 @@ +package com.hsdiary + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.hsdiary.ui.navigation.AppNavigation +import com.hsdiary.ui.theme.HSDiaryTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + HSDiaryTheme { + AppNavigation() + } + } + } +} diff --git a/app/src/main/java/com/hsdiary/data/db/AppDatabase.kt b/app/src/main/java/com/hsdiary/data/db/AppDatabase.kt new file mode 100644 index 0000000..fc5f287 --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/AppDatabase.kt @@ -0,0 +1,133 @@ +package com.hsdiary.data.db + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.sqlite.db.SupportSQLiteDatabase +import com.hsdiary.data.db.dao.* +import com.hsdiary.data.db.entity.* + +@Database( + entities = [ + ProfileEntity::class, + DayLogEntity::class, + ConditionEntryEntity::class, + CycleRecordEntity::class, + IntimacyLogEntity::class, + ConditionDefinitionEntity::class + ], + version = 1, + exportSchema = false +) +abstract class AppDatabase : RoomDatabase() { + abstract fun profileDao(): ProfileDao + abstract fun dayLogDao(): DayLogDao + abstract fun conditionDao(): ConditionDao + abstract fun cycleRecordDao(): CycleRecordDao + abstract fun intimacyLogDao(): IntimacyLogDao + abstract fun conditionDefinitionDao(): ConditionDefinitionDao + + companion object { + val seedCallback = object : Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + seedConditionDefinitions(db) + } + + private fun seedConditionDefinitions(db: SupportSQLiteDatabase) { + data class Seed(val key: String, val name: String, val cat: String, val vis: String, val order: Int) + + val seeds = listOf( + // Head & Neurological + Seed("HEADACHE", "Headache", "HEAD_NEUROLOGICAL", "ALL", 0), + Seed("MIGRAINE", "Migraine", "HEAD_NEUROLOGICAL", "ALL", 1), + Seed("BRAIN_FOG", "Brain fog", "HEAD_NEUROLOGICAL", "ALL", 2), + Seed("DIZZINESS", "Dizziness", "HEAD_NEUROLOGICAL", "ALL", 3), + Seed("FAINTING", "Fainting / near-fainting", "HEAD_NEUROLOGICAL", "ALL", 4), + Seed("TINNITUS", "Tinnitus", "HEAD_NEUROLOGICAL", "ALL", 5), + Seed("VISION_DISTURBANCE", "Vision disturbance", "HEAD_NEUROLOGICAL", "ALL", 6), + Seed("NUMBNESS_TINGLING", "Numbness / tingling", "HEAD_NEUROLOGICAL", "ALL", 7), + // Digestive + Seed("NAUSEA", "Nausea", "DIGESTIVE", "ALL", 0), + Seed("VOMITING", "Vomiting", "DIGESTIVE", "ALL", 1), + Seed("BLOATING", "Bloating", "DIGESTIVE", "ALL", 2), + Seed("CONSTIPATION", "Constipation", "DIGESTIVE", "ALL", 3), + Seed("DIARRHEA", "Diarrhea", "DIGESTIVE", "ALL", 4), + Seed("ACID_REFLUX", "Acid reflux / heartburn", "DIGESTIVE", "ALL", 5), + Seed("APPETITE_LOSS", "Appetite loss", "DIGESTIVE", "ALL", 6), + Seed("APPETITE_INCREASE", "Appetite increase", "DIGESTIVE", "ALL", 7), + Seed("ABDOMINAL_CRAMPING", "Abdominal cramping", "DIGESTIVE", "ALL", 8), + Seed("GAS", "Gas", "DIGESTIVE", "ALL", 9), + // Musculoskeletal + Seed("BACK_PAIN", "Back pain", "MUSCULOSKELETAL", "ALL", 0), + Seed("NECK_PAIN", "Neck pain", "MUSCULOSKELETAL", "ALL", 1), + Seed("JOINT_PAIN", "Joint pain", "MUSCULOSKELETAL", "ALL", 2), + Seed("MUSCLE_ACHES", "Muscle aches", "MUSCULOSKELETAL", "ALL", 3), + Seed("MUSCLE_CRAMPS", "Muscle cramps", "MUSCULOSKELETAL", "ALL", 4), + Seed("STIFFNESS", "Stiffness", "MUSCULOSKELETAL", "ALL", 5), + Seed("SWOLLEN_JOINTS", "Swelling in joints", "MUSCULOSKELETAL", "ALL", 6), + // Respiratory + Seed("SHORTNESS_OF_BREATH", "Shortness of breath", "RESPIRATORY", "ALL", 0), + Seed("CHEST_TIGHTNESS", "Chest tightness", "RESPIRATORY", "ALL", 1), + Seed("COUGH", "Cough", "RESPIRATORY", "ALL", 2), + Seed("CONGESTION", "Congestion", "RESPIRATORY", "ALL", 3), + Seed("SORE_THROAT", "Sore throat", "RESPIRATORY", "ALL", 4), + Seed("WHEEZING", "Wheezing", "RESPIRATORY", "ALL", 5), + // Cardiovascular + Seed("HEART_PALPITATIONS", "Heart palpitations", "CARDIOVASCULAR", "ALL", 0), + Seed("CHEST_PAIN", "Chest pain", "CARDIOVASCULAR", "ALL", 1), + Seed("RAPID_HEARTBEAT", "Rapid heartbeat", "CARDIOVASCULAR", "ALL", 2), + Seed("LOW_BP_SYMPTOMS", "Low blood pressure symptoms", "CARDIOVASCULAR", "ALL", 3), + Seed("SWOLLEN_ANKLES", "Swollen ankles / feet", "CARDIOVASCULAR", "ALL", 4), + // Energy & Sleep + Seed("FATIGUE", "Fatigue", "ENERGY_SLEEP", "ALL", 0), + Seed("EXHAUSTION", "Exhaustion", "ENERGY_SLEEP", "ALL", 1), + Seed("INSOMNIA", "Insomnia", "ENERGY_SLEEP", "ALL", 2), + Seed("HYPERSOMNIA", "Hypersomnia (sleeping too much)", "ENERGY_SLEEP", "ALL", 3), + Seed("RESTLESS_SLEEP", "Restless sleep", "ENERGY_SLEEP", "ALL", 4), + Seed("NIGHT_SWEATS", "Night sweats", "ENERGY_SLEEP", "ALL", 5), + // Mood & Mental + Seed("ANXIOUS", "Anxious", "MOOD_MENTAL", "ALL", 0), + Seed("IRRITABLE", "Irritable", "MOOD_MENTAL", "ALL", 1), + Seed("DEPRESSED", "Depressed", "MOOD_MENTAL", "ALL", 2), + Seed("MOOD_SWINGS", "Mood swings", "MOOD_MENTAL", "ALL", 3), + Seed("OVERWHELMED", "Overwhelmed", "MOOD_MENTAL", "ALL", 4), + Seed("CALM", "Calm", "MOOD_MENTAL", "ALL", 5), + Seed("HAPPY", "Happy", "MOOD_MENTAL", "ALL", 6), + Seed("PANIC_ATTACK", "Panic attack", "MOOD_MENTAL", "ALL", 7), + Seed("LOW_MOTIVATION", "Low motivation", "MOOD_MENTAL", "ALL", 8), + // Skin + Seed("RASH", "Rash", "SKIN", "ALL", 0), + Seed("HIVES", "Hives", "SKIN", "ALL", 1), + Seed("ACNE", "Acne breakout", "SKIN", "ALL", 2), + Seed("DRY_SKIN", "Dry skin", "SKIN", "ALL", 3), + Seed("EXCESSIVE_SWEATING", "Excessive sweating", "SKIN", "ALL", 4), + Seed("ITCHING", "Itching", "SKIN", "ALL", 5), + Seed("BRUISING", "Bruising easily", "SKIN", "ALL", 6), + // Female-Specific + Seed("CRAMPS", "Cramps", "FEMALE_SPECIFIC", "FEMALE_ONLY", 0), + Seed("BREAST_TENDERNESS", "Breast tenderness", "FEMALE_SPECIFIC", "FEMALE_ONLY", 1), + Seed("SPOTTING", "Spotting", "FEMALE_SPECIFIC", "FEMALE_ONLY", 2), + Seed("DISCHARGE_NORMAL", "Discharge — normal", "FEMALE_SPECIFIC", "FEMALE_ONLY", 3), + Seed("DISCHARGE_UNUSUAL", "Discharge — unusual", "FEMALE_SPECIFIC", "FEMALE_ONLY", 4), + Seed("HOT_FLASHES", "Hot flashes", "FEMALE_SPECIFIC", "FEMALE_ONLY", 5), + Seed("PMS", "PMS symptoms", "FEMALE_SPECIFIC", "FEMALE_ONLY", 6), + Seed("OVULATION_PAIN", "Ovulation pain (Mittelschmerz)", "FEMALE_SPECIFIC", "FEMALE_ONLY", 7), + // General + Seed("FEVER", "Fever", "GENERAL", "ALL", 0), + Seed("CHILLS", "Chills", "GENERAL", "ALL", 1), + Seed("DEHYDRATION", "Dehydration", "GENERAL", "ALL", 2), + Seed("ALLERGIC_REACTION", "Allergic reaction", "GENERAL", "ALL", 3), + Seed("COLD_FLU", "Cold / flu symptoms", "GENERAL", "ALL", 4), + Seed("SWOLLEN_LYMPH", "Swollen lymph nodes", "GENERAL", "ALL", 5) + ) + seeds.forEach { s -> + val name = s.name.replace("'", "''") + db.execSQL( + "INSERT INTO condition_definitions (condition_key, display_name, category, profile_visibility, sort_order) " + + "VALUES ('${s.key}', '$name', '${s.cat}', '${s.vis}', ${s.order})" + ) + } + } + } + } +} diff --git a/app/src/main/java/com/hsdiary/data/db/dao/ConditionDao.kt b/app/src/main/java/com/hsdiary/data/db/dao/ConditionDao.kt new file mode 100644 index 0000000..053ea9c --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/dao/ConditionDao.kt @@ -0,0 +1,33 @@ +package com.hsdiary.data.db.dao + +import androidx.room.* +import com.hsdiary.data.db.entity.ConditionEntryEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ConditionDao { + @Query("SELECT * FROM conditions WHERE day_log_id = :dayLogId") + fun getConditionsForDay(dayLogId: Long): Flow> + + @Query("SELECT * FROM conditions WHERE day_log_id = :dayLogId") + suspend fun getConditionsForDayOnce(dayLogId: Long): List + + @Query(""" + SELECT c.* FROM conditions c + JOIN day_logs dl ON c.day_log_id = dl.id + WHERE dl.profile_id = :profileId AND dl.date >= :startDate AND dl.date <= :endDate + """) + suspend fun getConditionsInRange(profileId: Long, startDate: String, endDate: String): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCondition(condition: ConditionEntryEntity): Long + + @Delete + suspend fun deleteCondition(condition: ConditionEntryEntity) + + @Query("DELETE FROM conditions WHERE day_log_id = :dayLogId AND condition_key = :conditionKey") + suspend fun deleteConditionByKey(dayLogId: Long, conditionKey: String) + + @Query("DELETE FROM conditions WHERE day_log_id = :dayLogId") + suspend fun deleteAllForDayLog(dayLogId: Long) +} diff --git a/app/src/main/java/com/hsdiary/data/db/dao/ConditionDefinitionDao.kt b/app/src/main/java/com/hsdiary/data/db/dao/ConditionDefinitionDao.kt new file mode 100644 index 0000000..d4fd4f6 --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/dao/ConditionDefinitionDao.kt @@ -0,0 +1,23 @@ +package com.hsdiary.data.db.dao + +import androidx.room.* +import com.hsdiary.data.db.entity.ConditionDefinitionEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ConditionDefinitionDao { + @Query("SELECT * FROM condition_definitions ORDER BY category, sort_order") + fun getAllDefinitions(): Flow> + + @Query("SELECT * FROM condition_definitions ORDER BY category, sort_order") + suspend fun getAllDefinitionsOnce(): List + + @Query("SELECT * FROM condition_definitions WHERE profile_visibility = 'ALL' OR profile_visibility = :visibility ORDER BY category, sort_order") + suspend fun getDefinitionsForProfile(visibility: String): List + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertAll(definitions: List) + + @Query("SELECT COUNT(*) FROM condition_definitions") + suspend fun getCount(): Int +} diff --git a/app/src/main/java/com/hsdiary/data/db/dao/CycleRecordDao.kt b/app/src/main/java/com/hsdiary/data/db/dao/CycleRecordDao.kt new file mode 100644 index 0000000..dd71913 --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/dao/CycleRecordDao.kt @@ -0,0 +1,41 @@ +package com.hsdiary.data.db.dao + +import androidx.room.* +import com.hsdiary.data.db.entity.CycleRecordEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface CycleRecordDao { + @Query("SELECT * FROM cycle_records WHERE profile_id = :profileId ORDER BY cycle_start ASC") + fun getCycleRecords(profileId: Long): Flow> + + @Query("SELECT * FROM cycle_records WHERE profile_id = :profileId ORDER BY cycle_start ASC") + suspend fun getCycleRecordsOnce(profileId: Long): List + + @Query("SELECT * FROM cycle_records WHERE profile_id = :profileId ORDER BY cycle_start DESC LIMIT 1") + suspend fun getLatestCycleRecord(profileId: Long): CycleRecordEntity? + + @Query("SELECT * FROM cycle_records WHERE profile_id = :profileId AND cycle_end IS NULL ORDER BY cycle_start DESC LIMIT 1") + suspend fun getCurrentCycleRecord(profileId: Long): CycleRecordEntity? + + @Query("SELECT * FROM cycle_records WHERE profile_id = :profileId AND cycle_start = :date LIMIT 1") + suspend fun getByStartDate(profileId: Long, date: String): CycleRecordEntity? + + @Query("SELECT * FROM cycle_records WHERE profile_id = :profileId AND cycle_end = :date LIMIT 1") + suspend fun getByEndDate(profileId: Long, date: String): CycleRecordEntity? + + @Query("SELECT * FROM cycle_records WHERE profile_id = :profileId AND cycle_start <= :date AND (cycle_end IS NULL OR cycle_end >= :date) LIMIT 1") + suspend fun getRecordContainingDate(profileId: Long, date: String): CycleRecordEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCycleRecord(record: CycleRecordEntity): Long + + @Update + suspend fun updateCycleRecord(record: CycleRecordEntity) + + @Delete + suspend fun deleteCycleRecord(record: CycleRecordEntity) + + @Query("DELETE FROM cycle_records WHERE profile_id = :profileId") + suspend fun deleteAllForProfile(profileId: Long) +} diff --git a/app/src/main/java/com/hsdiary/data/db/dao/DayLogDao.kt b/app/src/main/java/com/hsdiary/data/db/dao/DayLogDao.kt new file mode 100644 index 0000000..b53a0dc --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/dao/DayLogDao.kt @@ -0,0 +1,32 @@ +package com.hsdiary.data.db.dao + +import androidx.room.* +import com.hsdiary.data.db.entity.DayLogEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface DayLogDao { + @Query("SELECT * FROM day_logs WHERE profile_id = :profileId AND date = :date") + suspend fun getDayLog(profileId: Long, date: String): DayLogEntity? + + @Query("SELECT * FROM day_logs WHERE profile_id = :profileId AND date = :date") + fun getDayLogFlow(profileId: Long, date: String): Flow + + @Query("SELECT * FROM day_logs WHERE profile_id = :profileId AND date >= :startDate AND date <= :endDate ORDER BY date ASC") + fun getDayLogsInRange(profileId: Long, startDate: String, endDate: String): Flow> + + @Query("SELECT * FROM day_logs WHERE profile_id = :profileId AND date >= :startDate AND date <= :endDate ORDER BY date ASC") + suspend fun getDayLogsInRangeOnce(profileId: Long, startDate: String, endDate: String): List + + @Query("SELECT * FROM day_logs WHERE profile_id = :profileId AND period_active = 1 ORDER BY date ASC") + suspend fun getPeriodDays(profileId: Long): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertDayLog(dayLog: DayLogEntity): Long + + @Update + suspend fun updateDayLog(dayLog: DayLogEntity) + + @Query("DELETE FROM day_logs WHERE profile_id = :profileId") + suspend fun deleteAllForProfile(profileId: Long) +} diff --git a/app/src/main/java/com/hsdiary/data/db/dao/IntimacyLogDao.kt b/app/src/main/java/com/hsdiary/data/db/dao/IntimacyLogDao.kt new file mode 100644 index 0000000..f66723e --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/dao/IntimacyLogDao.kt @@ -0,0 +1,32 @@ +package com.hsdiary.data.db.dao + +import androidx.room.* +import com.hsdiary.data.db.entity.IntimacyLogEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface IntimacyLogDao { + @Query("SELECT * FROM intimacy_logs WHERE date = :date AND owner_profile_id = :profileId ORDER BY rowid ASC") + fun getLogsForDay(date: String, profileId: Long): Flow> + + @Query("SELECT * FROM intimacy_logs WHERE date = :date AND owner_profile_id = :profileId ORDER BY rowid ASC") + suspend fun getLogsForDayOnce(date: String, profileId: Long): List + + @Query("SELECT DISTINCT date FROM intimacy_logs WHERE owner_profile_id = :profileId AND date >= :startDate AND date <= :endDate") + suspend fun getDatesWithLogs(profileId: Long, startDate: String, endDate: String): List + + @Query("SELECT * FROM intimacy_logs WHERE date >= :startDate AND date <= :endDate AND owner_profile_id = :profileId ORDER BY date ASC") + suspend fun getLogsInRange(profileId: Long, startDate: String, endDate: String): List + + @Insert + suspend fun insertLog(log: IntimacyLogEntity): Long + + @Update + suspend fun updateLog(log: IntimacyLogEntity) + + @Delete + suspend fun deleteLog(log: IntimacyLogEntity) + + @Query("DELETE FROM intimacy_logs WHERE owner_profile_id = :profileId") + suspend fun deleteAllForProfile(profileId: Long) +} diff --git a/app/src/main/java/com/hsdiary/data/db/dao/ProfileDao.kt b/app/src/main/java/com/hsdiary/data/db/dao/ProfileDao.kt new file mode 100644 index 0000000..68528da --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/dao/ProfileDao.kt @@ -0,0 +1,29 @@ +package com.hsdiary.data.db.dao + +import androidx.room.* +import com.hsdiary.data.db.entity.ProfileEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ProfileDao { + @Query("SELECT * FROM profiles ORDER BY created_at ASC") + fun getAllProfiles(): Flow> + + @Query("SELECT * FROM profiles ORDER BY created_at ASC") + suspend fun getAllProfilesOnce(): List + + @Query("SELECT * FROM profiles WHERE id = :id") + suspend fun getProfileById(id: Long): ProfileEntity? + + @Query("SELECT COUNT(*) FROM profiles") + suspend fun getProfileCount(): Int + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertProfile(profile: ProfileEntity): Long + + @Update + suspend fun updateProfile(profile: ProfileEntity) + + @Delete + suspend fun deleteProfile(profile: ProfileEntity) +} diff --git a/app/src/main/java/com/hsdiary/data/db/entity/ConditionDefinitionEntity.kt b/app/src/main/java/com/hsdiary/data/db/entity/ConditionDefinitionEntity.kt new file mode 100644 index 0000000..94ea12d --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/entity/ConditionDefinitionEntity.kt @@ -0,0 +1,15 @@ +package com.hsdiary.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "condition_definitions") +data class ConditionDefinitionEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo(name = "condition_key") val conditionKey: String, + @ColumnInfo(name = "display_name") val displayName: String, + val category: String, + @ColumnInfo(name = "profile_visibility") val profileVisibility: String, + @ColumnInfo(name = "sort_order") val sortOrder: Int +) diff --git a/app/src/main/java/com/hsdiary/data/db/entity/ConditionEntryEntity.kt b/app/src/main/java/com/hsdiary/data/db/entity/ConditionEntryEntity.kt new file mode 100644 index 0000000..63aeece --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/entity/ConditionEntryEntity.kt @@ -0,0 +1,24 @@ +package com.hsdiary.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "conditions", + foreignKeys = [ForeignKey( + entity = DayLogEntity::class, + parentColumns = ["id"], + childColumns = ["day_log_id"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index("day_log_id")] +) +data class ConditionEntryEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo(name = "day_log_id") val dayLogId: Long, + @ColumnInfo(name = "condition_key") val conditionKey: String, + val rating: Int = 3 +) diff --git a/app/src/main/java/com/hsdiary/data/db/entity/CycleRecordEntity.kt b/app/src/main/java/com/hsdiary/data/db/entity/CycleRecordEntity.kt new file mode 100644 index 0000000..1e0c293 --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/entity/CycleRecordEntity.kt @@ -0,0 +1,27 @@ +package com.hsdiary.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "cycle_records", + foreignKeys = [ForeignKey( + entity = ProfileEntity::class, + parentColumns = ["id"], + childColumns = ["profile_id"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index("profile_id")] +) +data class CycleRecordEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo(name = "profile_id") val profileId: Long, + @ColumnInfo(name = "cycle_start") val cycleStart: String, + @ColumnInfo(name = "cycle_end") val cycleEnd: String? = null, + @ColumnInfo(name = "cycle_length") val cycleLength: Int? = null, + @ColumnInfo(name = "predicted_start") val predictedStart: String? = null, + @ColumnInfo(name = "delta_days") val deltaDays: Int? = null +) diff --git a/app/src/main/java/com/hsdiary/data/db/entity/DayLogEntity.kt b/app/src/main/java/com/hsdiary/data/db/entity/DayLogEntity.kt new file mode 100644 index 0000000..2ed2ede --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/entity/DayLogEntity.kt @@ -0,0 +1,25 @@ +package com.hsdiary.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "day_logs", + foreignKeys = [ForeignKey( + entity = ProfileEntity::class, + parentColumns = ["id"], + childColumns = ["profile_id"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index("profile_id"), Index(value = ["profile_id", "date"], unique = true)] +) +data class DayLogEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo(name = "profile_id") val profileId: Long, + val date: String, + @ColumnInfo(name = "period_active") val periodActive: Boolean = false, + val notes: String? = null +) diff --git a/app/src/main/java/com/hsdiary/data/db/entity/IntimacyLogEntity.kt b/app/src/main/java/com/hsdiary/data/db/entity/IntimacyLogEntity.kt new file mode 100644 index 0000000..cc05fc3 --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/entity/IntimacyLogEntity.kt @@ -0,0 +1,28 @@ +package com.hsdiary.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "intimacy_logs", + foreignKeys = [ForeignKey( + entity = ProfileEntity::class, + parentColumns = ["id"], + childColumns = ["owner_profile_id"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index("owner_profile_id")] +) +data class IntimacyLogEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val date: String, + @ColumnInfo(name = "owner_profile_id") val ownerProfileId: Long, + @ColumnInfo(name = "participant_type") val participantType: String, + @ColumnInfo(name = "participant_name") val participantName: String? = null, + @ColumnInfo(name = "time_of_day") val timeOfDay: String? = null, + val protected: Boolean = true, + val shared: Boolean = true +) diff --git a/app/src/main/java/com/hsdiary/data/db/entity/ProfileEntity.kt b/app/src/main/java/com/hsdiary/data/db/entity/ProfileEntity.kt new file mode 100644 index 0000000..9958683 --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/db/entity/ProfileEntity.kt @@ -0,0 +1,17 @@ +package com.hsdiary.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "profiles") +data class ProfileEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val name: String, + @ColumnInfo(name = "avatar_color") val avatarColor: String, + @ColumnInfo(name = "profile_type") val profileType: String, + @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(), + @ColumnInfo(name = "cycle_length_default") val cycleLengthDefault: Int = 28, + @ColumnInfo(name = "reproductive_status") val reproductiveStatus: String = "{}" +) diff --git a/app/src/main/java/com/hsdiary/data/model/ConditionCategory.kt b/app/src/main/java/com/hsdiary/data/model/ConditionCategory.kt new file mode 100644 index 0000000..1320442 --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/model/ConditionCategory.kt @@ -0,0 +1,14 @@ +package com.hsdiary.data.model + +enum class ConditionCategory(val displayName: String) { + HEAD_NEUROLOGICAL("Head & Neurological"), + DIGESTIVE("Digestive"), + MUSCULOSKELETAL("Musculoskeletal"), + RESPIRATORY("Respiratory"), + CARDIOVASCULAR("Cardiovascular"), + ENERGY_SLEEP("Energy & Sleep"), + MOOD_MENTAL("Mood & Mental"), + SKIN("Skin"), + FEMALE_SPECIFIC("Female-Specific"), + GENERAL("General") +} diff --git a/app/src/main/java/com/hsdiary/data/model/CyclePhase.kt b/app/src/main/java/com/hsdiary/data/model/CyclePhase.kt new file mode 100644 index 0000000..362b619 --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/model/CyclePhase.kt @@ -0,0 +1,13 @@ +package com.hsdiary.data.model + +enum class CyclePhase { + MENSTRUATION_CONFIRMED, + MENSTRUATION_PREDICTED, + FERTILE_WINDOW, + FERTILE_WINDOW_PREDICTED, + OVULATION, + OVULATION_PREDICTED, + LUTEAL, + FOLLICULAR, + NO_DATA +} diff --git a/app/src/main/java/com/hsdiary/data/model/ProfileType.kt b/app/src/main/java/com/hsdiary/data/model/ProfileType.kt new file mode 100644 index 0000000..1910a7e --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/model/ProfileType.kt @@ -0,0 +1,3 @@ +package com.hsdiary.data.model + +enum class ProfileType { FEMALE, MALE } diff --git a/app/src/main/java/com/hsdiary/data/model/ReproductiveStatus.kt b/app/src/main/java/com/hsdiary/data/model/ReproductiveStatus.kt new file mode 100644 index 0000000..5cb198e --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/model/ReproductiveStatus.kt @@ -0,0 +1,30 @@ +package com.hsdiary.data.model + +import kotlinx.serialization.Serializable + +enum class FemaleReproductiveStatus(val label: String) { + NORMAL("Normal / no known factors"), + HORMONAL_BC("Hormonal birth control"), + IUD_HORMONAL("IUD — hormonal"), + IUD_COPPER("IUD — copper"), + TUBAL_LIGATION("Tubal ligation"), + TRYING_TO_CONCEIVE("Trying to conceive"), + PREGNANT("Pregnant"), + POSTPARTUM("Postpartum"), + IRREGULAR("Irregular / perimenopause"), + OTHER("Other / prefer not to say") +} + +enum class MaleReproductiveStatus(val label: String) { + NORMAL("Normal / no known factors"), + VASECTOMY_CONFIRMED("Vasectomy — confirmed"), + VASECTOMY_PENDING("Vasectomy — awaiting confirmation"), + OTHER("Other / prefer not to say") +} + +@Serializable +data class ReproductiveStatusData( + val femaleStatus: String = FemaleReproductiveStatus.NORMAL.name, + val maleStatus: String = MaleReproductiveStatus.NORMAL.name, + val optionalDate: String? = null +) diff --git a/app/src/main/java/com/hsdiary/data/preferences/UserPreferences.kt b/app/src/main/java/com/hsdiary/data/preferences/UserPreferences.kt new file mode 100644 index 0000000..94fce84 --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/preferences/UserPreferences.kt @@ -0,0 +1,57 @@ +package com.hsdiary.data.preferences + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.preferencesDataStore +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +private val Context.dataStore: DataStore by preferencesDataStore(name = "hs_diary_prefs") + +@Singleton +class UserPreferences @Inject constructor( + @ApplicationContext private val context: Context +) { + private object Keys { + val ACTIVE_PROFILE_ID = longPreferencesKey("active_profile_id") + val FIRST_DAY_OF_WEEK = intPreferencesKey("first_day_of_week") // 1=Sunday, 2=Monday + val APP_THEME = stringPreferencesKey("app_theme") // LIGHT, DARK, SYSTEM + val ONBOARDING_COMPLETE = booleanPreferencesKey("onboarding_complete") + } + + val activeProfileId: Flow = context.dataStore.data.map { prefs -> + prefs[Keys.ACTIVE_PROFILE_ID] + } + + val firstDayOfWeek: Flow = context.dataStore.data.map { prefs -> + prefs[Keys.FIRST_DAY_OF_WEEK] ?: 1 + } + + val appTheme: Flow = context.dataStore.data.map { prefs -> + prefs[Keys.APP_THEME] ?: "SYSTEM" + } + + val onboardingComplete: Flow = context.dataStore.data.map { prefs -> + prefs[Keys.ONBOARDING_COMPLETE] ?: false + } + + suspend fun setActiveProfileId(id: Long) { + context.dataStore.edit { prefs -> prefs[Keys.ACTIVE_PROFILE_ID] = id } + } + + suspend fun setFirstDayOfWeek(day: Int) { + context.dataStore.edit { prefs -> prefs[Keys.FIRST_DAY_OF_WEEK] = day } + } + + suspend fun setAppTheme(theme: String) { + context.dataStore.edit { prefs -> prefs[Keys.APP_THEME] = theme } + } + + suspend fun setOnboardingComplete(complete: Boolean) { + context.dataStore.edit { prefs -> prefs[Keys.ONBOARDING_COMPLETE] = complete } + } +} diff --git a/app/src/main/java/com/hsdiary/data/repository/CycleRepository.kt b/app/src/main/java/com/hsdiary/data/repository/CycleRepository.kt new file mode 100644 index 0000000..725e6e2 --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/repository/CycleRepository.kt @@ -0,0 +1,69 @@ +package com.hsdiary.data.repository + +import com.hsdiary.data.db.dao.CycleRecordDao +import com.hsdiary.data.db.entity.CycleRecordEntity +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CycleRepository @Inject constructor(private val dao: CycleRecordDao) { + + fun getCycleRecords(profileId: Long): Flow> = + dao.getCycleRecords(profileId) + + suspend fun getCycleRecordsOnce(profileId: Long): List = + dao.getCycleRecordsOnce(profileId) + + suspend fun startNewCycle(profileId: Long, startDate: String, predictedStart: String?) { + val existing = dao.getCurrentCycleRecord(profileId) + if (existing != null) { + val length = java.time.LocalDate.parse(startDate) + .toEpochDay() + .minus(java.time.LocalDate.parse(existing.cycleStart).toEpochDay()) + .toInt() + val delta = predictedStart?.let { + java.time.LocalDate.parse(startDate) + .toEpochDay() + .minus(java.time.LocalDate.parse(it).toEpochDay()) + .toInt() + } + dao.updateCycleRecord(existing.copy(cycleEnd = startDate, cycleLength = length, deltaDays = delta)) + } + dao.insertCycleRecord(CycleRecordEntity( + profileId = profileId, + cycleStart = startDate, + predictedStart = predictedStart + )) + } + + suspend fun endCurrentCycle(profileId: Long, endDate: String) { + val current = dao.getCurrentCycleRecord(profileId) ?: return + val length = java.time.LocalDate.parse(endDate) + .toEpochDay() + .minus(java.time.LocalDate.parse(current.cycleStart).toEpochDay()) + .toInt() + 1 + dao.updateCycleRecord(current.copy(cycleEnd = endDate, cycleLength = length)) + } + + suspend fun removePeriodStart(profileId: Long, date: String) { + val record = dao.getByStartDate(profileId, date) ?: return + dao.deleteCycleRecord(record) + } + + suspend fun removePeriodEnd(profileId: Long, date: String) { + val record = dao.getByEndDate(profileId, date) ?: return + dao.updateCycleRecord(record.copy(cycleEnd = null, cycleLength = null)) + } + + suspend fun isPeriodStart(profileId: Long, date: String): Boolean = + dao.getByStartDate(profileId, date) != null + + suspend fun isPeriodEnd(profileId: Long, date: String): Boolean = + dao.getByEndDate(profileId, date) != null + + suspend fun getRecordContainingDate(profileId: Long, date: String): CycleRecordEntity? = + dao.getRecordContainingDate(profileId, date) + + suspend fun deleteAllForProfile(profileId: Long) = dao.deleteAllForProfile(profileId) +} diff --git a/app/src/main/java/com/hsdiary/data/repository/DayLogRepository.kt b/app/src/main/java/com/hsdiary/data/repository/DayLogRepository.kt new file mode 100644 index 0000000..973ceef --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/repository/DayLogRepository.kt @@ -0,0 +1,67 @@ +package com.hsdiary.data.repository + +import com.hsdiary.data.db.dao.ConditionDao +import com.hsdiary.data.db.dao.ConditionDefinitionDao +import com.hsdiary.data.db.dao.DayLogDao +import com.hsdiary.data.db.entity.ConditionEntryEntity +import com.hsdiary.data.db.entity.DayLogEntity +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DayLogRepository @Inject constructor( + private val dayLogDao: DayLogDao, + private val conditionDao: ConditionDao, + private val conditionDefinitionDao: ConditionDefinitionDao +) { + fun getDayLogFlow(profileId: Long, date: String): Flow = + dayLogDao.getDayLogFlow(profileId, date) + + fun getDayLogsInRange(profileId: Long, startDate: String, endDate: String): Flow> = + dayLogDao.getDayLogsInRange(profileId, startDate, endDate) + + suspend fun getDayLogsInRangeOnce(profileId: Long, startDate: String, endDate: String): List = + dayLogDao.getDayLogsInRangeOnce(profileId, startDate, endDate) + + suspend fun getOrCreateDayLog(profileId: Long, date: String): DayLogEntity { + val existing = dayLogDao.getDayLog(profileId, date) + if (existing != null) return existing + val newLog = DayLogEntity(profileId = profileId, date = date) + val id = dayLogDao.insertDayLog(newLog) + return newLog.copy(id = id) + } + + suspend fun upsertDayLog(dayLog: DayLogEntity): Long { + return dayLogDao.insertDayLog(dayLog) + } + + suspend fun updateDayLog(dayLog: DayLogEntity) = dayLogDao.updateDayLog(dayLog) + + fun getConditionsForDay(dayLogId: Long): Flow> = + conditionDao.getConditionsForDay(dayLogId) + + suspend fun getConditionsForDayOnce(dayLogId: Long): List = + conditionDao.getConditionsForDayOnce(dayLogId) + + suspend fun setCondition(dayLogId: Long, conditionKey: String, rating: Int) { + conditionDao.deleteConditionByKey(dayLogId, conditionKey) + conditionDao.insertCondition(ConditionEntryEntity(dayLogId = dayLogId, conditionKey = conditionKey, rating = rating)) + } + + suspend fun removeCondition(dayLogId: Long, conditionKey: String) { + conditionDao.deleteConditionByKey(dayLogId, conditionKey) + } + + suspend fun getConditionsInRange(profileId: Long, startDate: String, endDate: String) = + conditionDao.getConditionsInRange(profileId, startDate, endDate) + + suspend fun getDefinitionsForProfile(isFemale: Boolean) = + conditionDefinitionDao.getDefinitionsForProfile(if (isFemale) "FEMALE_ONLY" else "NONE") + + suspend fun getAllDefinitions() = conditionDefinitionDao.getAllDefinitionsOnce() + + suspend fun getPeriodDays(profileId: Long) = dayLogDao.getPeriodDays(profileId) + + suspend fun deleteAllForProfile(profileId: Long) = dayLogDao.deleteAllForProfile(profileId) +} diff --git a/app/src/main/java/com/hsdiary/data/repository/IntimacyRepository.kt b/app/src/main/java/com/hsdiary/data/repository/IntimacyRepository.kt new file mode 100644 index 0000000..cf758ad --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/repository/IntimacyRepository.kt @@ -0,0 +1,28 @@ +package com.hsdiary.data.repository + +import com.hsdiary.data.db.dao.IntimacyLogDao +import com.hsdiary.data.db.entity.IntimacyLogEntity +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IntimacyRepository @Inject constructor(private val dao: IntimacyLogDao) { + + fun getLogsForDay(date: String, profileId: Long): Flow> = + dao.getLogsForDay(date, profileId) + + suspend fun getLogsForDayOnce(date: String, profileId: Long) = + dao.getLogsForDayOnce(date, profileId) + + suspend fun getDatesWithLogs(profileId: Long, startDate: String, endDate: String): List = + dao.getDatesWithLogs(profileId, startDate, endDate) + + suspend fun insertLog(log: IntimacyLogEntity): Long = dao.insertLog(log) + + suspend fun updateLog(log: IntimacyLogEntity) = dao.updateLog(log) + + suspend fun deleteLog(log: IntimacyLogEntity) = dao.deleteLog(log) + + suspend fun deleteAllForProfile(profileId: Long) = dao.deleteAllForProfile(profileId) +} diff --git a/app/src/main/java/com/hsdiary/data/repository/ProfileRepository.kt b/app/src/main/java/com/hsdiary/data/repository/ProfileRepository.kt new file mode 100644 index 0000000..5cb8bac --- /dev/null +++ b/app/src/main/java/com/hsdiary/data/repository/ProfileRepository.kt @@ -0,0 +1,25 @@ +package com.hsdiary.data.repository + +import com.hsdiary.data.db.dao.ProfileDao +import com.hsdiary.data.db.entity.ProfileEntity +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ProfileRepository @Inject constructor(private val dao: ProfileDao) { + + fun getAllProfiles(): Flow> = dao.getAllProfiles() + + suspend fun getAllProfilesOnce(): List = dao.getAllProfilesOnce() + + suspend fun getProfileById(id: Long): ProfileEntity? = dao.getProfileById(id) + + suspend fun insertProfile(profile: ProfileEntity): Long = dao.insertProfile(profile) + + suspend fun updateProfile(profile: ProfileEntity) = dao.updateProfile(profile) + + suspend fun deleteProfile(profile: ProfileEntity) = dao.deleteProfile(profile) + + suspend fun getProfileCount(): Int = dao.getProfileCount() +} diff --git a/app/src/main/java/com/hsdiary/di/AppModule.kt b/app/src/main/java/com/hsdiary/di/AppModule.kt new file mode 100644 index 0000000..85c689d --- /dev/null +++ b/app/src/main/java/com/hsdiary/di/AppModule.kt @@ -0,0 +1,24 @@ +package com.hsdiary.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import javax.inject.Qualifier +import javax.inject.Singleton + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class ApplicationScope + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + @ApplicationScope + fun provideApplicationScope(): CoroutineScope = CoroutineScope(SupervisorJob()) +} diff --git a/app/src/main/java/com/hsdiary/di/DatabaseModule.kt b/app/src/main/java/com/hsdiary/di/DatabaseModule.kt new file mode 100644 index 0000000..caeb521 --- /dev/null +++ b/app/src/main/java/com/hsdiary/di/DatabaseModule.kt @@ -0,0 +1,32 @@ +package com.hsdiary.di + +import android.content.Context +import androidx.room.Room +import com.hsdiary.data.db.AppDatabase +import com.hsdiary.data.db.dao.* +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + @Provides + @Singleton + fun provideDatabase(@ApplicationContext context: Context): AppDatabase { + return Room.databaseBuilder(context, AppDatabase::class.java, "hs_diary.db") + .addCallback(AppDatabase.seedCallback) + .build() + } + + @Provides fun provideProfileDao(db: AppDatabase): ProfileDao = db.profileDao() + @Provides fun provideDayLogDao(db: AppDatabase): DayLogDao = db.dayLogDao() + @Provides fun provideConditionDao(db: AppDatabase): ConditionDao = db.conditionDao() + @Provides fun provideCycleRecordDao(db: AppDatabase): CycleRecordDao = db.cycleRecordDao() + @Provides fun provideIntimacyLogDao(db: AppDatabase): IntimacyLogDao = db.intimacyLogDao() + @Provides fun provideConditionDefinitionDao(db: AppDatabase): ConditionDefinitionDao = db.conditionDefinitionDao() +} diff --git a/app/src/main/java/com/hsdiary/domain/CyclePredictionEngine.kt b/app/src/main/java/com/hsdiary/domain/CyclePredictionEngine.kt new file mode 100644 index 0000000..b749811 --- /dev/null +++ b/app/src/main/java/com/hsdiary/domain/CyclePredictionEngine.kt @@ -0,0 +1,159 @@ +package com.hsdiary.domain + +import com.hsdiary.data.db.entity.CycleRecordEntity +import com.hsdiary.data.model.CyclePhase +import com.hsdiary.domain.model.CyclePrediction +import java.time.LocalDate +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.math.roundToInt + +@Singleton +class CyclePredictionEngine @Inject constructor() { + + fun buildPrediction( + records: List, + defaultCycleLength: Int, + today: LocalDate = LocalDate.now(), + rangeStart: LocalDate = today.minusMonths(1), + rangeEnd: LocalDate = today.plusMonths(2) + ): CyclePrediction { + if (records.isEmpty()) return buildDefaultPrediction(defaultCycleLength, today, rangeStart, rangeEnd) + + val completedLengths = records.mapNotNull { it.cycleLength } + val avgLength = if (completedLengths.isEmpty()) defaultCycleLength + else completedLengths.takeLast(12).average().roundToInt() + + val tier = when { + completedLengths.size >= 12 -> 4 + completedLengths.size >= 4 -> 3 + records.isNotEmpty() -> 2 + else -> 1 + } + + val latestStart = records.maxByOrNull { it.cycleStart }?.let { LocalDate.parse(it.cycleStart) } + ?: return buildDefaultPrediction(defaultCycleLength, today, rangeStart, rangeEnd) + + val cycleDay = (today.toEpochDay() - latestStart.toEpochDay()).toInt() + 1 + val nextPeriod = latestStart.plusDays(avgLength.toLong()) + val lutealLength = 14 + val ovulation = latestStart.plusDays((avgLength - lutealLength).toLong()) + val fertileStart = ovulation.minusDays(5) + val fertileEnd = ovulation.plusDays(1) + + val phaseMap = buildPhaseMap( + records = records, + latestStart = latestStart, + avgLength = avgLength, + ovulation = ovulation, + fertileStart = fertileStart, + fertileEnd = fertileEnd, + rangeStart = rangeStart, + rangeEnd = rangeEnd + ) + + val currentPhase = phaseMap[today] ?: CyclePhase.NO_DATA + + return CyclePrediction( + currentCycleStartDate = latestStart, + currentCycleDay = maxOf(1, cycleDay), + currentPhase = currentPhase, + nextPeriodDate = nextPeriod, + fertileWindowStart = fertileStart, + fertileWindowEnd = fertileEnd, + ovulationDate = ovulation, + averageCycleLength = avgLength, + cyclesLogged = records.size, + tier = tier, + daysUntilNextPeriod = (nextPeriod.toEpochDay() - today.toEpochDay()).toInt().takeIf { it >= 0 }, + phaseMap = phaseMap + ) + } + + private fun buildDefaultPrediction( + defaultLength: Int, + today: LocalDate, + rangeStart: LocalDate, + rangeEnd: LocalDate + ): CyclePrediction { + return CyclePrediction( + currentCycleStartDate = null, + currentCycleDay = 0, + currentPhase = CyclePhase.NO_DATA, + nextPeriodDate = null, + fertileWindowStart = null, + fertileWindowEnd = null, + ovulationDate = null, + averageCycleLength = defaultLength, + cyclesLogged = 0, + tier = 1, + daysUntilNextPeriod = null, + phaseMap = emptyMap() + ) + } + + private fun buildPhaseMap( + records: List, + latestStart: LocalDate, + avgLength: Int, + ovulation: LocalDate, + fertileStart: LocalDate, + fertileEnd: LocalDate, + rangeStart: LocalDate, + rangeEnd: LocalDate + ): Map { + val map = mutableMapOf() + + // Mark confirmed period days from actual records + records.forEach { record -> + val start = LocalDate.parse(record.cycleStart) + val end = record.cycleEnd?.let { LocalDate.parse(it) } ?: start.plusDays(4) + var d = start + while (!d.isAfter(end) && !d.isAfter(rangeEnd)) { + if (!d.isBefore(rangeStart)) map[d] = CyclePhase.MENSTRUATION_CONFIRMED + d = d.plusDays(1) + } + } + + // Project forward from latestStart through rangeEnd in cycle increments + var cycleStart = latestStart + while (cycleStart.isBefore(rangeEnd)) { + val cycleOvulation = cycleStart.plusDays((avgLength - 14).toLong()) + val cycleFertileStart = cycleOvulation.minusDays(5) + val cycleFertileEnd = cycleOvulation.plusDays(1) + val cycleEnd = cycleStart.plusDays(avgLength.toLong()) + val periodEnd = cycleStart.plusDays(4) + + // Period days (predicted if future, already marked confirmed if past) + var d = cycleStart + while (!d.isAfter(periodEnd) && !d.isAfter(rangeEnd)) { + if (!d.isBefore(rangeStart) && map[d] == null) { + map[d] = CyclePhase.MENSTRUATION_PREDICTED + } + d = d.plusDays(1) + } + + // Fertile window + d = cycleFertileStart + while (!d.isAfter(cycleFertileEnd) && !d.isAfter(rangeEnd)) { + if (!d.isBefore(rangeStart) && map[d] == null) { + map[d] = if (d == cycleOvulation) CyclePhase.OVULATION_PREDICTED else CyclePhase.FERTILE_WINDOW_PREDICTED + } + d = d.plusDays(1) + } + + // Luteal phase + d = cycleFertileEnd.plusDays(1) + while (!d.isBefore(cycleStart) && !d.isAfter(cycleEnd.minusDays(1)) && !d.isAfter(rangeEnd)) { + if (!d.isBefore(rangeStart) && map[d] == null) { + map[d] = CyclePhase.LUTEAL + } + d = d.plusDays(1) + } + + cycleStart = cycleEnd + } + + return map + } +} diff --git a/app/src/main/java/com/hsdiary/domain/model/CyclePrediction.kt b/app/src/main/java/com/hsdiary/domain/model/CyclePrediction.kt new file mode 100644 index 0000000..f0718c5 --- /dev/null +++ b/app/src/main/java/com/hsdiary/domain/model/CyclePrediction.kt @@ -0,0 +1,24 @@ +package com.hsdiary.domain.model + +import com.hsdiary.data.model.CyclePhase +import java.time.LocalDate + +data class CyclePrediction( + val currentCycleStartDate: LocalDate?, + val currentCycleDay: Int, + val currentPhase: CyclePhase, + val nextPeriodDate: LocalDate?, + val fertileWindowStart: LocalDate?, + val fertileWindowEnd: LocalDate?, + val ovulationDate: LocalDate?, + val averageCycleLength: Int, + val cyclesLogged: Int, + val tier: Int, + val daysUntilNextPeriod: Int?, + val phaseMap: Map +) + +data class DayPhaseInfo( + val phase: CyclePhase, + val cycleDay: Int? +) diff --git a/app/src/main/java/com/hsdiary/ui/calendar/CalendarScreen.kt b/app/src/main/java/com/hsdiary/ui/calendar/CalendarScreen.kt new file mode 100644 index 0000000..dfad647 --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/calendar/CalendarScreen.kt @@ -0,0 +1,330 @@ +package com.hsdiary.ui.calendar + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.TrendingUp +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import com.hsdiary.data.model.CyclePhase +import com.hsdiary.data.model.ProfileType +import com.hsdiary.ui.components.AvatarDot +import com.hsdiary.ui.components.ProfileSwitchSheet +import com.hsdiary.ui.theme.* +import java.time.LocalDate +import java.time.YearMonth +import java.time.format.TextStyle +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CalendarScreen( + onDayClick: (String) -> Unit, + onInsightsClick: () -> Unit, + onTrendsClick: () -> Unit, + onSettingsClick: () -> Unit, + viewModel: CalendarViewModel = hiltViewModel() +) { + val state by viewModel.uiState.collectAsState() + val activeProfile = state.activeProfile + + Scaffold( + topBar = { + TopAppBar( + title = { + Column { + Text("H&S Diary", style = MaterialTheme.typography.titleLarge) + if (activeProfile != null) { + Text( + activeProfile.name, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + }, + actions = { + IconButton(onClick = onTrendsClick) { + Icon(Icons.AutoMirrored.Filled.TrendingUp, contentDescription = "Trends") + } + IconButton(onClick = onSettingsClick) { + Icon(Icons.Default.Settings, contentDescription = "Settings") + } + if (activeProfile != null) { + val avatarColor = parseColor(activeProfile.avatarColor) + IconButton(onClick = { viewModel.showProfileSheet() }) { + AvatarDot(name = activeProfile.name, avatarColor = avatarColor, size = 32) + } + } + } + ) + } + ) { padding -> + Column(modifier = Modifier.fillMaxSize().padding(padding)) { + // Context banner (female only) + if (activeProfile?.profileType == ProfileType.FEMALE.name) { + val prediction = state.prediction + ContextBanner( + prediction = prediction, + onClick = onInsightsClick + ) + } + + // Month navigation + MonthHeader( + month = state.currentMonth, + onPrevious = viewModel::previousMonth, + onNext = viewModel::nextMonth + ) + + // Day-of-week headers + val dayHeaders = if (state.firstDayOfWeek == 1) + listOf("S","M","T","W","T","F","S") + else + listOf("M","T","W","T","F","S","S") + Row(Modifier.fillMaxWidth().padding(horizontal = 8.dp)) { + dayHeaders.forEach { h -> + Text( + text = h, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // Calendar grid + val isFemale = activeProfile?.profileType == ProfileType.FEMALE.name + CalendarGrid( + days = state.dayStates, + isFemale = isFemale, + onDayClick = { onDayClick(it.toString()) } + ) + } + } + + if (state.showProfileSheet && state.profiles.size > 1) { + ProfileSwitchSheet( + profiles = viewModel.getProfileSwitchItems(state), + onProfileSelected = viewModel::switchProfile, + onDismiss = viewModel::hideProfileSheet + ) + } +} + +@Composable +private fun ContextBanner( + prediction: com.hsdiary.domain.model.CyclePrediction?, + onClick: () -> Unit +) { + val text = when { + prediction == null || prediction.cyclesLogged == 0 -> + "🩸 Set up your cycle — log your first period to begin" + prediction.currentPhase == CyclePhase.MENSTRUATION_CONFIRMED || + prediction.currentPhase == CyclePhase.MENSTRUATION_PREDICTED -> + "🩸 Period · Day ${prediction.currentCycleDay}" + prediction.currentPhase == CyclePhase.OVULATION || + prediction.currentPhase == CyclePhase.OVULATION_PREDICTED -> + "🌿 Ovulation day" + prediction.currentPhase == CyclePhase.FERTILE_WINDOW || + prediction.currentPhase == CyclePhase.FERTILE_WINDOW_PREDICTED -> { + val remaining = prediction.fertileWindowEnd?.let { + (it.toEpochDay() - LocalDate.now().toEpochDay()).toInt() + } ?: 0 + "🌿 Fertile window · ~$remaining days remaining" + } + else -> { + val days = prediction.daysUntilNextPeriod + if (days != null) "🩸 Next period in ~$days days · Cycle day ${prediction.currentCycleDay}" + else "🩸 Cycle day ${prediction.currentCycleDay}" + } + } + + Surface( + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), + color = MaterialTheme.colorScheme.primaryContainer, + tonalElevation = 1.dp + ) { + Text( + text = text, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } +} + +@Composable +private fun MonthHeader(month: YearMonth, onPrevious: () -> Unit, onNext: () -> Unit) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = onPrevious) { + Icon(Icons.Default.ChevronLeft, contentDescription = "Previous month") + } + Text( + text = "${month.month.getDisplayName(TextStyle.FULL, Locale.getDefault())} ${month.year}", + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + IconButton(onClick = onNext) { + Icon(Icons.Default.ChevronRight, contentDescription = "Next month") + } + } +} + +@Composable +private fun CalendarGrid( + days: List, + isFemale: Boolean, + onDayClick: (LocalDate) -> Unit +) { + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp)) { + days.chunked(7).forEach { week -> + Row(modifier = Modifier.fillMaxWidth()) { + week.forEach { day -> + DayCell( + day = day, + isFemale = isFemale, + onClick = { onDayClick(day.date) }, + modifier = Modifier.weight(1f) + ) + } + } + } + } +} + +@Composable +private fun DayCell( + day: DayState, + isFemale: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val phaseColor = if (isFemale) phaseColor(day.phase) else null + val textAlpha = if (day.isCurrentMonth) 1f else 0.35f + + Box( + modifier = modifier + .aspectRatio(1f) + .padding(1.dp) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick) + ) { + // Phase color band at bottom + if (phaseColor != null) { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.25f) + .align(Alignment.BottomCenter) + .background( + phaseColor.copy( + alpha = if (isPredicted(day.phase)) 0.45f else 0.75f + ) + ) + ) + } + + // Today ring + if (day.isToday) { + Box( + modifier = Modifier + .size(28.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary) + .align(Alignment.TopCenter) + .offset(y = 3.dp) + ) + } + + Column( + modifier = Modifier.fillMaxSize().padding(2.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Day number + Text( + text = day.date.dayOfMonth.toString(), + style = MaterialTheme.typography.bodySmall, + color = if (day.isToday) Color.White else MaterialTheme.colorScheme.onSurface.copy(alpha = textAlpha), + fontWeight = if (day.isToday) FontWeight.Bold else FontWeight.Normal, + modifier = Modifier.padding(top = 4.dp) + ) + + // Icon row + val icons = buildIconList(day, isFemale) + if (icons.isNotEmpty()) { + Row( + modifier = Modifier.padding(top = 1.dp), + horizontalArrangement = Arrangement.Center + ) { + val visibleIcons = if (day.hasIntimacy) icons.take(2) else icons.take(3) + val overflow = icons.size - visibleIcons.size + visibleIcons.forEach { icon -> + Text(icon, fontSize = 9.sp, lineHeight = 10.sp) + } + if (overflow > 0) Text("+$overflow", fontSize = 7.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) + if (day.hasIntimacy) Text("â¤ï¸", fontSize = 9.sp, lineHeight = 10.sp) + } + } + } + } +} + +private fun buildIconList(day: DayState, isFemale: Boolean): List { + val icons = mutableListOf() + if (isFemale && (day.periodActive || day.phase == CyclePhase.MENSTRUATION_CONFIRMED || day.phase == CyclePhase.MENSTRUATION_PREDICTED)) { + icons.add("🩸") + } + day.conditionKeys.take(3).forEach { key -> + icons.add(conditionIcon(key)) + } + return icons +} + +private fun conditionIcon(key: String): String = when { + key.contains("HEAD") || key.contains("MIGRAINE") -> "âš¡" + key.contains("FATIGUE") || key.contains("EXHAUST") -> "😴" + key.contains("NAUSEA") || key.contains("VOMIT") -> "🤢" + key.contains("CRAMP") -> "💫" + key.contains("BACK") || key.contains("NECK") || key.contains("JOINT") -> "🦴" + key.contains("MOOD") || key.contains("ANXIOUS") || key.contains("IRRITABLE") -> "😤" + key.contains("HAPPY") || key.contains("CALM") -> "😊" + key.contains("FEVER") || key.contains("CHILL") -> "🤒" + else -> "•" +} + +private fun phaseColor(phase: CyclePhase): Color? = when (phase) { + CyclePhase.MENSTRUATION_CONFIRMED -> PeriodColor + CyclePhase.MENSTRUATION_PREDICTED -> PeriodPredictedColor + CyclePhase.FERTILE_WINDOW, CyclePhase.FERTILE_WINDOW_PREDICTED -> FertileColor + CyclePhase.OVULATION, CyclePhase.OVULATION_PREDICTED -> OvulationColor + CyclePhase.LUTEAL -> LutealColor + else -> null +} + +private fun isPredicted(phase: CyclePhase): Boolean = phase == CyclePhase.MENSTRUATION_PREDICTED || + phase == CyclePhase.FERTILE_WINDOW_PREDICTED || phase == CyclePhase.OVULATION_PREDICTED + +fun parseColor(hex: String): Color = try { + Color(android.graphics.Color.parseColor(hex)) +} catch (e: Exception) { Color(0xFFE91E63) } diff --git a/app/src/main/java/com/hsdiary/ui/calendar/CalendarViewModel.kt b/app/src/main/java/com/hsdiary/ui/calendar/CalendarViewModel.kt new file mode 100644 index 0000000..93ece03 --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/calendar/CalendarViewModel.kt @@ -0,0 +1,207 @@ +package com.hsdiary.ui.calendar + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hsdiary.data.db.entity.DayLogEntity +import com.hsdiary.data.db.entity.ProfileEntity +import com.hsdiary.data.model.CyclePhase +import com.hsdiary.data.model.ProfileType +import com.hsdiary.data.preferences.UserPreferences +import com.hsdiary.data.repository.CycleRepository +import com.hsdiary.data.repository.DayLogRepository +import com.hsdiary.data.repository.IntimacyRepository +import com.hsdiary.data.repository.ProfileRepository +import com.hsdiary.domain.CyclePredictionEngine +import com.hsdiary.domain.model.CyclePrediction +import com.hsdiary.ui.components.ProfileSwitchItem +import com.hsdiary.ui.theme.AvatarColors +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.time.LocalDate +import java.time.YearMonth +import javax.inject.Inject + +data class DayState( + val date: LocalDate, + val isToday: Boolean, + val isCurrentMonth: Boolean, + val periodActive: Boolean, + val phase: CyclePhase, + val conditionKeys: List, + val hasIntimacy: Boolean +) + +data class CalendarUiState( + val activeProfile: ProfileEntity? = null, + val profiles: List = emptyList(), + val currentMonth: YearMonth = YearMonth.now(), + val dayStates: List = emptyList(), + val prediction: CyclePrediction? = null, + val firstDayOfWeek: Int = 1, + val showProfileSheet: Boolean = false +) + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltViewModel +class CalendarViewModel @Inject constructor( + private val profileRepository: ProfileRepository, + private val dayLogRepository: DayLogRepository, + private val cycleRepository: CycleRepository, + private val intimacyRepository: IntimacyRepository, + private val predictionEngine: CyclePredictionEngine, + private val userPreferences: UserPreferences +) : ViewModel() { + + private val _currentMonth = MutableStateFlow(YearMonth.now()) + private val _showProfileSheet = MutableStateFlow(false) + + val uiState: StateFlow = combine( + userPreferences.activeProfileId, + profileRepository.getAllProfiles(), + _currentMonth, + userPreferences.firstDayOfWeek, + _showProfileSheet + ) { activeId, profiles, month, fdow, showSheet -> + Triple(activeId to profiles, month to fdow, showSheet) + }.flatMapLatest { (profileData, monthData, showSheet) -> + val (activeId, profiles) = profileData + val (month, fdow) = monthData + val activeProfile = profiles.find { it.id == activeId } ?: profiles.firstOrNull() + + if (activeProfile == null) { + return@flatMapLatest flowOf(CalendarUiState(showProfileSheet = showSheet)) + } + + val isFemale = activeProfile.profileType == ProfileType.FEMALE.name + val rangeStart = month.atDay(1).minusDays(6) + val rangeEnd = month.atEndOfMonth().plusDays(6) + + combine( + dayLogRepository.getDayLogsInRange(activeProfile.id, rangeStart.toString(), rangeEnd.toString()), + cycleRepository.getCycleRecords(activeProfile.id) + ) { dayLogs, cycleRecords -> + val today = LocalDate.now() + val prediction = if (isFemale) { + predictionEngine.buildPrediction( + records = cycleRecords, + defaultCycleLength = activeProfile.cycleLengthDefault, + today = today, + rangeStart = rangeStart, + rangeEnd = rangeEnd + ) + } else null + + val intimacyDates = intimacyRepository.getDatesWithLogs( + activeProfile.id, rangeStart.toString(), rangeEnd.toString() + ).toSet() + + val dayLogMap = dayLogs.associateBy { it.date } + val conditionMap = buildConditionMap(dayLogs, activeProfile.id, rangeStart.toString(), rangeEnd.toString()) + + val days = buildCalendarDays(month, fdow, today, dayLogMap, conditionMap, intimacyDates, prediction) + + CalendarUiState( + activeProfile = activeProfile, + profiles = profiles, + currentMonth = month, + dayStates = days, + prediction = prediction, + firstDayOfWeek = fdow, + showProfileSheet = showSheet + ) + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), CalendarUiState()) + + private suspend fun buildConditionMap( + dayLogs: List, + profileId: Long, + startDate: String, + endDate: String + ): Map> { + val allConditions = dayLogRepository.getConditionsInRange(profileId, startDate, endDate) + val dayLogById = dayLogs.associateBy { it.id } + return allConditions + .groupBy { entry -> dayLogById[entry.dayLogId]?.date ?: "" } + .filter { it.key.isNotEmpty() } + .mapValues { (_, conds) -> conds.map { it.conditionKey } } + } + + private fun buildCalendarDays( + month: YearMonth, + firstDayOfWeek: Int, + today: LocalDate, + dayLogMap: Map, + conditionMap: Map>, + intimacyDates: Set, + prediction: CyclePrediction? + ): List { + val firstOfMonth = month.atDay(1) + // offset: how many blank days before the 1st + val dayOfWeek = firstOfMonth.dayOfWeek.value % 7 // 0=Sun..6=Sat + val offset = if (firstDayOfWeek == 1) dayOfWeek else (dayOfWeek + 6) % 7 + val result = mutableListOf() + + // preceding month days + for (i in offset - 1 downTo 0) { + val date = firstOfMonth.minusDays(i.toLong() + 1) + result.add(makeDayState(date, today, false, dayLogMap, conditionMap, intimacyDates, prediction)) + } + // current month + for (day in 1..month.lengthOfMonth()) { + val date = month.atDay(day) + result.add(makeDayState(date, today, true, dayLogMap, conditionMap, intimacyDates, prediction)) + } + // trailing days to complete last row + val remaining = (7 - result.size % 7) % 7 + for (i in 1..remaining) { + val date = month.atEndOfMonth().plusDays(i.toLong()) + result.add(makeDayState(date, today, false, dayLogMap, conditionMap, intimacyDates, prediction)) + } + return result + } + + private fun makeDayState( + date: LocalDate, + today: LocalDate, + isCurrentMonth: Boolean, + dayLogMap: Map, + conditionMap: Map>, + intimacyDates: Set, + prediction: CyclePrediction? + ): DayState { + val key = date.toString() + val log = dayLogMap[key] + val phase = prediction?.phaseMap?.get(date) ?: CyclePhase.NO_DATA + return DayState( + date = date, + isToday = date == today, + isCurrentMonth = isCurrentMonth, + periodActive = log?.periodActive ?: false, + phase = phase, + conditionKeys = conditionMap[key] ?: emptyList(), + hasIntimacy = intimacyDates.contains(key) + ) + } + + fun previousMonth() { _currentMonth.value = _currentMonth.value.minusMonths(1) } + fun nextMonth() { _currentMonth.value = _currentMonth.value.plusMonths(1) } + + fun showProfileSheet() { _showProfileSheet.value = true } + fun hideProfileSheet() { _showProfileSheet.value = false } + + fun switchProfile(profileId: Long) { + viewModelScope.launch { userPreferences.setActiveProfileId(profileId) } + } + + fun getProfileSwitchItems(state: CalendarUiState): List { + return state.profiles.map { p -> + val color = try { + android.graphics.Color.parseColor(p.avatarColor) + .let { androidx.compose.ui.graphics.Color(it) } + } catch (e: Exception) { AvatarColors[0] } + ProfileSwitchItem(p.id, p.name, color, p.id == state.activeProfile?.id) + } + } +} diff --git a/app/src/main/java/com/hsdiary/ui/components/ProfileChip.kt b/app/src/main/java/com/hsdiary/ui/components/ProfileChip.kt new file mode 100644 index 0000000..c739961 --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/components/ProfileChip.kt @@ -0,0 +1,77 @@ +package com.hsdiary.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun ProfileChip( + name: String, + avatarColor: Color, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .clip(MaterialTheme.shapes.medium) + .clickable(onClick = onClick) + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Box( + modifier = Modifier + .size(28.dp) + .clip(CircleShape) + .background(avatarColor), + contentAlignment = Alignment.Center + ) { + Text( + text = name.take(1).uppercase(), + color = Color.White, + fontSize = 13.sp, + fontWeight = FontWeight.Bold + ) + } + Text( + text = name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + } +} + +@Composable +fun AvatarDot( + name: String, + avatarColor: Color, + size: Int = 36, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .size(size.dp) + .clip(CircleShape) + .background(avatarColor), + contentAlignment = Alignment.Center + ) { + Text( + text = name.take(1).uppercase(), + color = Color.White, + fontSize = (size * 0.38).sp, + fontWeight = FontWeight.Bold + ) + } +} diff --git a/app/src/main/java/com/hsdiary/ui/components/ProfileSwitchSheet.kt b/app/src/main/java/com/hsdiary/ui/components/ProfileSwitchSheet.kt new file mode 100644 index 0000000..0879df2 --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/components/ProfileSwitchSheet.kt @@ -0,0 +1,78 @@ +package com.hsdiary.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +data class ProfileSwitchItem( + val id: Long, + val name: String, + val avatarColor: Color, + val isActive: Boolean +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProfileSwitchSheet( + profiles: List, + onProfileSelected: (Long) -> Unit, + onDismiss: () -> Unit +) { + var confirmTarget by remember { mutableStateOf(null) } + + ModalBottomSheet(onDismissRequest = onDismiss) { + Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 32.dp)) { + Text( + text = "Switch Profile", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + profiles.forEach { profile -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + if (!profile.isActive) confirmTarget = profile + else onDismiss() + } + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + AvatarDot(name = profile.name, avatarColor = profile.avatarColor) + Text( + text = profile.name, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + if (profile.isActive) { + Badge { Text("Active") } + } + } + } + } + } + + confirmTarget?.let { target -> + AlertDialog( + onDismissRequest = { confirmTarget = null }, + title = { Text("Switch Profile") }, + text = { Text("Switch to ${target.name}?") }, + confirmButton = { + TextButton(onClick = { + onProfileSelected(target.id) + confirmTarget = null + onDismiss() + }) { Text("Switch") } + }, + dismissButton = { + TextButton(onClick = { confirmTarget = null }) { Text("Cancel") } + } + ) + } +} diff --git a/app/src/main/java/com/hsdiary/ui/daydetail/DayDetailScreen.kt b/app/src/main/java/com/hsdiary/ui/daydetail/DayDetailScreen.kt new file mode 100644 index 0000000..33ff1a3 --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/daydetail/DayDetailScreen.kt @@ -0,0 +1,516 @@ +package com.hsdiary.ui.daydetail + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.hsdiary.data.db.entity.ConditionDefinitionEntity +import com.hsdiary.data.db.entity.IntimacyLogEntity +import com.hsdiary.data.model.CyclePhase +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DayDetailScreen( + date: String, + onBack: () -> Unit, + viewModel: DayDetailViewModel = hiltViewModel() +) { + val state by viewModel.uiState.collectAsState() + var savedSnackbar by remember { mutableStateOf(false) } + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(savedSnackbar) { + if (savedSnackbar) { + snackbarHostState.showSnackbar("Saved") + savedSnackbar = false + } + } + + val dateLabel = remember(date) { + LocalDate.parse(date).format(DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy", Locale.getDefault())) + } + + BackHandler { + if (state.isDirty) { + viewModel.saveAndExit() + savedSnackbar = true + } + onBack() + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + TopAppBar( + title = { Text(dateLabel, style = MaterialTheme.typography.titleMedium) }, + navigationIcon = { + IconButton(onClick = { + if (state.isDirty) viewModel.saveAndExit() + onBack() + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") + } + } + ) + } + ) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize().padding(padding), + contentPadding = PaddingValues(bottom = 32.dp) + ) { + // Cycle section (female only) + if (state.isFemale) { + item { + CycleSection( + state = state, + onTogglePeriodStart = viewModel::togglePeriodStart, + onTogglePeriodEnd = viewModel::togglePeriodEnd + ) + } + item { HorizontalDivider() } + } + + // Conditions section + item { + ConditionsSection( + definitions = state.definitions, + selectedConditions = state.conditions, + onToggle = viewModel::toggleCondition, + onRatingChange = viewModel::setConditionRating + ) + } + + // Notes + item { + NotesSection( + notes = state.notes, + onNotesChange = viewModel::updateNotes + ) + } + + item { HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) } + + // Intimacy section + item { + IntimacySection( + logs = state.intimacyLogs, + profiles = state.allProfiles, + activeProfileId = state.activeProfile?.id ?: 0L, + isFemaleInFertileWindow = state.isFemale && ( + state.currentPhase == CyclePhase.FERTILE_WINDOW || + state.currentPhase == CyclePhase.OVULATION + ), + onAdd = { pType, pName, time, protected -> + viewModel.addIntimacyLog(pType, pName, time, protected) + }, + onDelete = viewModel::deleteIntimacyLog + ) + } + } + } +} + +@Composable +private fun CycleSection( + state: DayDetailUiState, + onTogglePeriodStart: () -> Unit, + onTogglePeriodEnd: () -> Unit +) { + Column(modifier = Modifier.padding(16.dp)) { + // Phase / cycle day header + val phaseLabel = when (state.currentPhase) { + CyclePhase.MENSTRUATION_CONFIRMED -> "Menstruation" + CyclePhase.MENSTRUATION_PREDICTED -> "Predicted: Menstruation" + CyclePhase.FERTILE_WINDOW, CyclePhase.FERTILE_WINDOW_PREDICTED -> "Fertile Window" + CyclePhase.OVULATION, CyclePhase.OVULATION_PREDICTED -> "Ovulation" + CyclePhase.LUTEAL -> "Luteal Phase" + CyclePhase.FOLLICULAR -> "Follicular Phase" + else -> null + } + if (state.cycleDay > 0 && phaseLabel != null) { + Text( + "Cycle day ${state.cycleDay} · $phaseLabel", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(4.dp)) + } + + // Period day-of-period badge + if (state.periodActive && state.periodDayNumber > 0) { + Text( + "Period day ${state.periodDayNumber}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) + Spacer(Modifier.height(8.dp)) + } + + Text("Period", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + Spacer(Modifier.height(12.dp)) + + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + FilterChip( + selected = state.isPeriodStart, + onClick = onTogglePeriodStart, + label = { Text("Period started") }, + leadingIcon = { + Icon( + if (state.isPeriodStart) Icons.Default.Check else Icons.Default.PlayArrow, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } + ) + FilterChip( + selected = state.isPeriodEnd, + onClick = onTogglePeriodEnd, + enabled = state.isPeriodStart || state.periodActive || state.isPeriodEnd, + label = { Text("Period ended") }, + leadingIcon = { + Icon( + if (state.isPeriodEnd) Icons.Default.Check else Icons.Default.Stop, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } + ) + } + + // Predicted-period nudge + if (state.currentPhase == CyclePhase.MENSTRUATION_PREDICTED && !state.isPeriodStart) { + Spacer(Modifier.height(8.dp)) + OutlinedCard(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon(Icons.Default.Info, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp)) + Text("Period predicted today — tap \"Period started\" to confirm", style = MaterialTheme.typography.bodySmall) + } + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ConditionsSection( + definitions: List, + selectedConditions: Map, + onToggle: (String) -> Unit, + onRatingChange: (String, Int) -> Unit +) { + val grouped = remember(definitions) { + definitions.groupBy { it.category } + } + var expandedCategories by remember { mutableStateOf(setOf()) } + + Column(modifier = Modifier.padding(16.dp)) { + Text("Symptoms", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + Spacer(Modifier.height(12.dp)) + + grouped.forEach { (category, items) -> + val categoryLabel = category.replace("_", " ").lowercase() + .replaceFirstChar { it.uppercase() } + val isExpanded = category in expandedCategories + val hasSelected = items.any { selectedConditions.containsKey(it.conditionKey) } + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + expandedCategories = if (isExpanded) + expandedCategories - category + else + expandedCategories + category + } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = categoryLabel, + style = MaterialTheme.typography.labelLarge, + color = if (hasSelected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f) + ) + if (hasSelected) { + Badge(containerColor = MaterialTheme.colorScheme.primary) { + Text("${items.count { selectedConditions.containsKey(it.conditionKey) }}") + } + } + Icon( + if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + + if (isExpanded) { + FlowRow( + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items.forEach { def -> + val selected = selectedConditions.containsKey(def.conditionKey) + FilterChip( + selected = selected, + onClick = { onToggle(def.conditionKey) }, + label = { Text(def.displayName, style = MaterialTheme.typography.bodySmall) } + ) + } + } + // Rating row for selected items in this category + items.filter { selectedConditions.containsKey(it.conditionKey) }.forEach { def -> + val rating = selectedConditions[def.conditionKey] ?: 3 + RatingRow( + label = def.displayName, + rating = rating, + onRatingChange = { onRatingChange(def.conditionKey, it) } + ) + } + } + } + } +} + +@Composable +private fun RatingRow(label: String, rating: Int, onRatingChange: (Int) -> Unit) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(label, style = MaterialTheme.typography.bodySmall, modifier = Modifier.width(120.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + (1..5).forEach { i -> + Box( + modifier = Modifier + .size(22.dp) + .background( + if (i <= rating) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(4.dp) + ) + .clickable { onRatingChange(i) }, + contentAlignment = Alignment.Center + ) { + Text( + i.toString(), + style = MaterialTheme.typography.labelSmall, + color = if (i <= rating) MaterialTheme.colorScheme.onPrimary + else MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } +} + +@Composable +private fun NotesSection(notes: String, onNotesChange: (String) -> Unit) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + OutlinedTextField( + value = notes, + onValueChange = { if (it.length <= 256) onNotesChange(it) }, + label = { Text("Notes") }, + modifier = Modifier.fillMaxWidth(), + minLines = 2, + maxLines = 4, + supportingText = { Text("${notes.length}/256") } + ) + } +} + +@Composable +private fun IntimacySection( + logs: List, + profiles: List, + activeProfileId: Long, + isFemaleInFertileWindow: Boolean, + onAdd: (String, String?, String?, Boolean) -> Unit, + onDelete: (IntimacyLogEntity) -> Unit +) { + var isExpanded by remember { mutableStateOf(true) } + var showAddForm by remember { mutableStateOf(false) } + + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { isExpanded = !isExpanded } + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Intimacy", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f) + ) + Icon( + if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, + contentDescription = null + ) + } + + if (isExpanded) { + logs.forEach { log -> + IntimacyLogCard( + log = log, + isFemaleInFertileWindow = isFemaleInFertileWindow, + onDelete = { onDelete(log) } + ) + } + + if (showAddForm) { + AddIntimacyForm( + profiles = profiles, + activeProfileId = activeProfileId, + onConfirm = { pType, pName, time, protected -> + onAdd(pType, pName, time, protected) + showAddForm = false + }, + onCancel = { showAddForm = false } + ) + } else { + TextButton( + onClick = { showAddForm = true }, + modifier = Modifier.padding(top = 4.dp) + ) { + Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp)) + Spacer(Modifier.width(4.dp)) + Text("Add encounter") + } + } + } + } +} + +@Composable +private fun IntimacyLogCard( + log: IntimacyLogEntity, + isFemaleInFertileWindow: Boolean, + onDelete: () -> Unit +) { + OutlinedCard(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text("â¤ï¸ · ${log.participantName ?: log.participantType} · ${log.timeOfDay ?: ""} · ${if (log.protected) "Protected" else "Unprotected"}", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.weight(1f) + ) + IconButton(onClick = onDelete, modifier = Modifier.size(20.dp)) { + Icon(Icons.Default.Close, contentDescription = "Delete", modifier = Modifier.size(14.dp)) + } + } + if (isFemaleInFertileWindow && !log.protected) { + Text( + "🌿 Unprotected · Fertile window", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.padding(start = 12.dp, bottom = 8.dp) + ) + } + } +} + +@Composable +private fun AddIntimacyForm( + profiles: List, + activeProfileId: Long, + onConfirm: (String, String?, String?, Boolean) -> Unit, + onCancel: () -> Unit +) { + var participantType by remember { mutableStateOf("OTHER") } + var participantName by remember { mutableStateOf("") } + var timeOfDay by remember { mutableStateOf("") } + var protected by remember { mutableStateOf(true) } + + val otherProfile = profiles.firstOrNull { it.id != activeProfileId } + + Card(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("New encounter", style = MaterialTheme.typography.titleSmall) + + // Participant selector + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if (otherProfile != null) { + FilterChip( + selected = participantType == "PARTNER", + onClick = { participantType = "PARTNER"; participantName = otherProfile.name }, + label = { Text(otherProfile.name) } + ) + } + FilterChip( + selected = participantType == "OTHER", + onClick = { participantType = "OTHER"; participantName = "" }, + label = { Text("Other") } + ) + } + if (participantType == "OTHER") { + OutlinedTextField( + value = participantName, + onValueChange = { if (it.length <= 32) participantName = it }, + label = { Text("Name (optional)") }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + } + OutlinedTextField( + value = timeOfDay, + onValueChange = { timeOfDay = it }, + label = { Text("Time (optional, HH:MM)") }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text("Protected", style = MaterialTheme.typography.bodyMedium) + Switch(checked = protected, onCheckedChange = { protected = it }) + } + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedButton(onClick = onCancel, modifier = Modifier.weight(1f)) { Text("Cancel") } + Button( + onClick = { + onConfirm( + participantType, + participantName.ifBlank { null }, + timeOfDay.ifBlank { null }, + protected + ) + }, + modifier = Modifier.weight(1f) + ) { Text("Add") } + } + } + } +} + +@Composable +private fun BackHandler(onBack: () -> Unit) { + androidx.activity.compose.BackHandler(onBack = onBack) +} diff --git a/app/src/main/java/com/hsdiary/ui/daydetail/DayDetailViewModel.kt b/app/src/main/java/com/hsdiary/ui/daydetail/DayDetailViewModel.kt new file mode 100644 index 0000000..48c7b26 --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/daydetail/DayDetailViewModel.kt @@ -0,0 +1,258 @@ +package com.hsdiary.ui.daydetail + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hsdiary.data.db.entity.ConditionDefinitionEntity +import com.hsdiary.data.db.entity.DayLogEntity +import com.hsdiary.data.db.entity.IntimacyLogEntity +import com.hsdiary.data.db.entity.ProfileEntity +import com.hsdiary.data.model.CyclePhase +import com.hsdiary.data.model.ProfileType +import com.hsdiary.data.preferences.UserPreferences +import com.hsdiary.data.repository.CycleRepository +import com.hsdiary.data.repository.DayLogRepository +import com.hsdiary.data.repository.IntimacyRepository +import com.hsdiary.data.repository.ProfileRepository +import com.hsdiary.domain.CyclePredictionEngine +import com.hsdiary.domain.model.CyclePrediction +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.time.LocalDate +import javax.inject.Inject + +data class DayDetailUiState( + val date: LocalDate = LocalDate.now(), + val activeProfile: ProfileEntity? = null, + val allProfiles: List = emptyList(), + val dayLog: DayLogEntity? = null, + val periodActive: Boolean = false, + val isPeriodStart: Boolean = false, + val isPeriodEnd: Boolean = false, + val periodDayNumber: Int = 0, // day-of-period for this date (1 = first day) + val notes: String = "", + val conditions: Map = emptyMap(), + val definitions: List = emptyList(), + val intimacyLogs: List = emptyList(), + val prediction: CyclePrediction? = null, + val isFemale: Boolean = false, + val cycleDay: Int = 0, + val currentPhase: CyclePhase = CyclePhase.NO_DATA, + val isDirty: Boolean = false +) + +@HiltViewModel +class DayDetailViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + private val profileRepository: ProfileRepository, + private val dayLogRepository: DayLogRepository, + private val cycleRepository: CycleRepository, + private val intimacyRepository: IntimacyRepository, + private val predictionEngine: CyclePredictionEngine, + private val userPreferences: UserPreferences +) : ViewModel() { + + private val dateStr: String = savedStateHandle["date"] ?: LocalDate.now().toString() + val date: LocalDate = LocalDate.parse(dateStr) + + private val _uiState = MutableStateFlow(DayDetailUiState(date = date)) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + viewModelScope.launch { loadData() } + } + + private suspend fun loadData() { + val activeId = userPreferences.activeProfileId.first() + val profiles = profileRepository.getAllProfilesOnce() + val activeProfile = profiles.find { it.id == activeId } ?: profiles.firstOrNull() ?: return + val isFemale = activeProfile.profileType == ProfileType.FEMALE.name + + val dayLog = dayLogRepository.getOrCreateDayLog(activeProfile.id, dateStr) + val conditions = dayLogRepository.getConditionsForDayOnce(dayLog.id) + .associate { it.conditionKey to it.rating } + val defsToShow = dayLogRepository.getDefinitionsForProfile(isFemale) + val intimacyLogs = intimacyRepository.getLogsForDayOnce(dateStr, activeProfile.id) + + val cycleRecords = cycleRepository.getCycleRecordsOnce(activeProfile.id) + val prediction = if (isFemale) { + predictionEngine.buildPrediction(cycleRecords, activeProfile.cycleLengthDefault, LocalDate.now()) + } else null + + val phase = prediction?.phaseMap?.get(date) ?: CyclePhase.NO_DATA + val cycleDay = if (isFemale && prediction?.currentCycleStartDate != null) { + maxOf(1, (date.toEpochDay() - prediction.currentCycleStartDate.toEpochDay()).toInt() + 1) + } else 0 + + val isPeriodStart = isFemale && cycleRepository.isPeriodStart(activeProfile.id, dateStr) + val isPeriodEnd = isFemale && cycleRepository.isPeriodEnd(activeProfile.id, dateStr) + + // Calculate day-of-period: how many days from the period start to this date + val periodDayNumber = if (isFemale && dayLog.periodActive) { + val record = cycleRepository.getRecordContainingDate(activeProfile.id, dateStr) + if (record != null) { + (date.toEpochDay() - LocalDate.parse(record.cycleStart).toEpochDay()).toInt() + 1 + } else 0 + } else 0 + + _uiState.value = DayDetailUiState( + date = date, + activeProfile = activeProfile, + allProfiles = profiles, + dayLog = dayLog, + periodActive = dayLog.periodActive, + isPeriodStart = isPeriodStart, + isPeriodEnd = isPeriodEnd, + periodDayNumber = periodDayNumber, + notes = dayLog.notes ?: "", + conditions = conditions, + definitions = defsToShow, + intimacyLogs = intimacyLogs, + prediction = prediction, + isFemale = isFemale, + cycleDay = cycleDay, + currentPhase = phase + ) + + intimacyRepository.getLogsForDay(dateStr, activeProfile.id) + .onEach { logs -> _uiState.value = _uiState.value.copy(intimacyLogs = logs) } + .launchIn(viewModelScope) + } + + fun togglePeriodStart() { + viewModelScope.launch { + val state = _uiState.value + val profileId = state.activeProfile?.id ?: return@launch + val dayLog = state.dayLog ?: return@launch + + if (state.isPeriodStart) { + // Remove: delete the cycle record that starts here, clear period_active on this day + cycleRepository.removePeriodStart(profileId, dateStr) + val updatedLog = dayLog.copy(periodActive = false) + dayLogRepository.upsertDayLog(updatedLog) + _uiState.value = state.copy( + isPeriodStart = false, + periodActive = false, + dayLog = updatedLog, + periodDayNumber = 0, + isDirty = true + ) + } else { + // Mark as start: create a new cycle record, set period_active = true + val predictedStart = state.prediction?.nextPeriodDate + cycleRepository.startNewCycle(profileId, dateStr, predictedStart?.toString()) + val updatedLog = dayLog.copy(periodActive = true) + dayLogRepository.upsertDayLog(updatedLog) + _uiState.value = state.copy( + isPeriodStart = true, + isPeriodEnd = false, + periodActive = true, + dayLog = updatedLog, + periodDayNumber = 1, + isDirty = true + ) + } + } + } + + fun togglePeriodEnd() { + viewModelScope.launch { + val state = _uiState.value + val profileId = state.activeProfile?.id ?: return@launch + val dayLog = state.dayLog ?: return@launch + + if (state.isPeriodEnd) { + // Remove end date from the cycle record + cycleRepository.removePeriodEnd(profileId, dateStr) + _uiState.value = state.copy(isPeriodEnd = false, isDirty = true) + } else { + // Mark as end: close the current open cycle record at this date + cycleRepository.endCurrentCycle(profileId, dateStr) + // Also mark this day as a period day if not already + if (!state.periodActive) { + val updatedLog = dayLog.copy(periodActive = true) + dayLogRepository.upsertDayLog(updatedLog) + _uiState.value = state.copy( + isPeriodEnd = true, + periodActive = true, + dayLog = updatedLog, + isDirty = true + ) + } else { + _uiState.value = state.copy(isPeriodEnd = true, isDirty = true) + } + } + } + } + + fun toggleCondition(conditionKey: String) { + viewModelScope.launch { + val state = _uiState.value + val dayLog = state.dayLog ?: return@launch + val current = state.conditions.toMutableMap() + if (current.containsKey(conditionKey)) { + dayLogRepository.removeCondition(dayLog.id, conditionKey) + current.remove(conditionKey) + } else { + dayLogRepository.setCondition(dayLog.id, conditionKey, 3) + current[conditionKey] = 3 + } + _uiState.value = state.copy(conditions = current, isDirty = true) + } + } + + fun setConditionRating(conditionKey: String, rating: Int) { + viewModelScope.launch { + val state = _uiState.value + val dayLog = state.dayLog ?: return@launch + dayLogRepository.setCondition(dayLog.id, conditionKey, rating) + val current = state.conditions.toMutableMap() + current[conditionKey] = rating + _uiState.value = state.copy(conditions = current, isDirty = true) + } + } + + fun updateNotes(notes: String) { + _uiState.value = _uiState.value.copy(notes = notes, isDirty = true) + } + + fun saveNotes() { + viewModelScope.launch { + val state = _uiState.value + val dayLog = state.dayLog ?: return@launch + dayLogRepository.upsertDayLog(dayLog.copy(notes = state.notes.ifBlank { null })) + } + } + + fun addIntimacyLog(participantType: String, participantName: String?, timeOfDay: String?, protected: Boolean) { + viewModelScope.launch { + val state = _uiState.value + val profileId = state.activeProfile?.id ?: return@launch + intimacyRepository.insertLog( + IntimacyLogEntity( + date = dateStr, + ownerProfileId = profileId, + participantType = participantType, + participantName = participantName, + timeOfDay = timeOfDay, + protected = protected, + shared = participantType != "OTHER" + ) + ) + _uiState.value = state.copy(isDirty = true) + } + } + + fun deleteIntimacyLog(log: IntimacyLogEntity) { + viewModelScope.launch { intimacyRepository.deleteLog(log) } + } + + fun saveAndExit() { + viewModelScope.launch { + val state = _uiState.value + val dayLog = state.dayLog ?: return@launch + dayLogRepository.upsertDayLog(dayLog.copy(notes = state.notes.ifBlank { null })) + } + } +} diff --git a/app/src/main/java/com/hsdiary/ui/insights/CycleInsightsScreen.kt b/app/src/main/java/com/hsdiary/ui/insights/CycleInsightsScreen.kt new file mode 100644 index 0000000..14eec3e --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/insights/CycleInsightsScreen.kt @@ -0,0 +1,207 @@ +package com.hsdiary.ui.insights + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.hsdiary.data.model.CyclePhase +import com.hsdiary.domain.model.CyclePrediction +import java.time.format.DateTimeFormatter +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CycleInsightsScreen( + onBack: () -> Unit, + viewModel: CycleInsightsViewModel = hiltViewModel() +) { + val state by viewModel.uiState.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Cycle Insights") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") + } + } + ) + } + ) { padding -> + if (state.isLoading) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + return@Scaffold + } + + val prediction = state.prediction + if (prediction == null || prediction.cyclesLogged == 0) { + Box( + Modifier.fillMaxSize().padding(padding).padding(32.dp), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text("🩸", style = MaterialTheme.typography.displayMedium) + Spacer(Modifier.height(16.dp)) + Text("No cycle data yet", style = MaterialTheme.typography.titleMedium) + Spacer(Modifier.height(8.dp)) + Text( + "Log your first period on the calendar to begin tracking.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + return@Scaffold + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Current phase card + ElevatedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Current Cycle", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) + Spacer(Modifier.height(8.dp)) + val phaseText = when (prediction.currentPhase) { + CyclePhase.MENSTRUATION_CONFIRMED, CyclePhase.MENSTRUATION_PREDICTED -> "🩸 Menstruation" + CyclePhase.FERTILE_WINDOW, CyclePhase.FERTILE_WINDOW_PREDICTED -> "🌿 Fertile Window" + CyclePhase.OVULATION, CyclePhase.OVULATION_PREDICTED -> "🌿 Ovulation Day" + CyclePhase.LUTEAL -> "🌙 Luteal Phase" + CyclePhase.FOLLICULAR -> "🌱 Follicular Phase" + else -> "—" + } + Text(phaseText, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.SemiBold) + if (prediction.currentCycleDay > 0) { + Text("Day ${prediction.currentCycleDay}", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } + + // Stats row + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { + StatCard( + label = "Avg Cycle", + value = "${prediction.averageCycleLength} days", + modifier = Modifier.weight(1f) + ) + StatCard( + label = "Cycles Logged", + value = "${prediction.cyclesLogged}", + modifier = Modifier.weight(1f) + ) + StatCard( + label = "Prediction", + value = "Tier ${prediction.tier}", + modifier = Modifier.weight(1f) + ) + } + + // Next period + prediction.nextPeriodDate?.let { next -> + ElevatedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Upcoming", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) + Spacer(Modifier.height(8.dp)) + val fmt = DateTimeFormatter.ofPattern("MMMM d", Locale.getDefault()) + Text("Next period: ${next.format(fmt)}", style = MaterialTheme.typography.bodyLarge) + prediction.daysUntilNextPeriod?.let { days -> + Text("In ~$days days", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + prediction.fertileWindowStart?.let { fw -> + val fwEnd = prediction.fertileWindowEnd + Text( + "Fertile window: ${fw.format(fmt)} – ${fwEnd?.format(fmt) ?: ""}", + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } + + // Cycle length bar chart + if (state.recentCycles.size >= 2) { + ElevatedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Cycle History", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) + Spacer(Modifier.height(12.dp)) + CycleLengthBarChart(cycles = state.recentCycles.mapNotNull { it.cycleLength }) + } + } + } + } + } +} + +@Composable +private fun StatCard(label: String, value: String, modifier: Modifier = Modifier) { + ElevatedCard(modifier = modifier) { + Column( + modifier = Modifier.padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(value, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } +} + +@Composable +private fun CycleLengthBarChart(cycles: List) { + if (cycles.isEmpty()) return + val maxLen = cycles.max().toFloat() + val barColor = MaterialTheme.colorScheme.primary + val avgColor = MaterialTheme.colorScheme.secondary + val avg = cycles.average().toFloat() + + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(100.dp) + ) { + val barWidth = (size.width / (cycles.size * 1.5f)) + val spacing = barWidth * 0.5f + cycles.forEachIndexed { i, length -> + val barHeight = (length / maxLen) * size.height * 0.85f + val x = i * (barWidth + spacing) + drawRect( + color = barColor, + topLeft = Offset(x, size.height - barHeight), + size = Size(barWidth, barHeight) + ) + } + // average line + val avgY = size.height - (avg / maxLen) * size.height * 0.85f + drawLine( + color = avgColor, + start = Offset(0f, avgY), + end = Offset(size.width, avgY), + strokeWidth = 2.dp.toPx() + ) + } + Spacer(Modifier.height(4.dp)) + Text( + "Last ${cycles.size} cycles · Average: ${avg.toInt()} days", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) +} diff --git a/app/src/main/java/com/hsdiary/ui/insights/CycleInsightsViewModel.kt b/app/src/main/java/com/hsdiary/ui/insights/CycleInsightsViewModel.kt new file mode 100644 index 0000000..76a4fdc --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/insights/CycleInsightsViewModel.kt @@ -0,0 +1,65 @@ +package com.hsdiary.ui.insights + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hsdiary.data.db.entity.CycleRecordEntity +import com.hsdiary.data.db.entity.ProfileEntity +import com.hsdiary.data.model.ProfileType +import com.hsdiary.data.preferences.UserPreferences +import com.hsdiary.data.repository.CycleRepository +import com.hsdiary.data.repository.ProfileRepository +import com.hsdiary.domain.CyclePredictionEngine +import com.hsdiary.domain.model.CyclePrediction +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.time.LocalDate +import javax.inject.Inject + +data class CycleInsightsUiState( + val profile: ProfileEntity? = null, + val prediction: CyclePrediction? = null, + val recentCycles: List = emptyList(), + val isLoading: Boolean = true +) + +@HiltViewModel +class CycleInsightsViewModel @Inject constructor( + private val profileRepository: ProfileRepository, + private val cycleRepository: CycleRepository, + private val predictionEngine: CyclePredictionEngine, + private val userPreferences: UserPreferences +) : ViewModel() { + + private val _uiState = MutableStateFlow(CycleInsightsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + viewModelScope.launch { load() } + } + + private suspend fun load() { + val activeId = userPreferences.activeProfileId.first() + val profiles = profileRepository.getAllProfilesOnce() + val profile = profiles.find { it.id == activeId } ?: profiles.firstOrNull() ?: return + if (profile.profileType != ProfileType.FEMALE.name) { + _uiState.value = CycleInsightsUiState(profile = profile, isLoading = false) + return + } + cycleRepository.getCycleRecords(profile.id) + .onEach { records -> + val prediction = predictionEngine.buildPrediction( + records = records, + defaultCycleLength = profile.cycleLengthDefault, + today = LocalDate.now() + ) + _uiState.value = CycleInsightsUiState( + profile = profile, + prediction = prediction, + recentCycles = records.takeLast(12), + isLoading = false + ) + } + .launchIn(viewModelScope) + } +} diff --git a/app/src/main/java/com/hsdiary/ui/navigation/AppNavigation.kt b/app/src/main/java/com/hsdiary/ui/navigation/AppNavigation.kt new file mode 100644 index 0000000..9012c8a --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/navigation/AppNavigation.kt @@ -0,0 +1,60 @@ +package com.hsdiary.ui.navigation + +import androidx.compose.runtime.* +import androidx.compose.runtime.collectAsState +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.hsdiary.ui.calendar.CalendarScreen +import com.hsdiary.ui.daydetail.DayDetailScreen +import com.hsdiary.ui.insights.CycleInsightsScreen +import com.hsdiary.ui.onboarding.OnboardingScreen +import com.hsdiary.ui.onboarding.OnboardingViewModel +import com.hsdiary.ui.settings.SettingsScreen +import com.hsdiary.ui.trends.HealthTrendsScreen + +@Composable +fun AppNavigation() { + val navController = rememberNavController() + val onboardingVm: OnboardingViewModel = hiltViewModel() + val onboardingComplete by onboardingVm.onboardingComplete.collectAsState(initial = null) + + if (onboardingComplete == null) return // wait for prefs to load + + NavHost( + navController = navController, + startDestination = if (onboardingComplete == true) Screen.Calendar.route else Screen.Onboarding.route + ) { + composable(Screen.Onboarding.route) { + OnboardingScreen( + onComplete = { + navController.navigate(Screen.Calendar.route) { + popUpTo(Screen.Onboarding.route) { inclusive = true } + } + } + ) + } + composable(Screen.Calendar.route) { + CalendarScreen( + onDayClick = { date -> navController.navigate(Screen.DayDetail.createRoute(date)) }, + onInsightsClick = { navController.navigate(Screen.CycleInsights.route) }, + onTrendsClick = { navController.navigate(Screen.HealthTrends.route) }, + onSettingsClick = { navController.navigate(Screen.Settings.route) } + ) + } + composable(Screen.DayDetail.route) { backstack -> + val date = backstack.arguments?.getString("date") ?: return@composable + DayDetailScreen(date = date, onBack = { navController.popBackStack() }) + } + composable(Screen.CycleInsights.route) { + CycleInsightsScreen(onBack = { navController.popBackStack() }) + } + composable(Screen.HealthTrends.route) { + HealthTrendsScreen(onBack = { navController.popBackStack() }) + } + composable(Screen.Settings.route) { + SettingsScreen(onBack = { navController.popBackStack() }) + } + } +} diff --git a/app/src/main/java/com/hsdiary/ui/navigation/Screen.kt b/app/src/main/java/com/hsdiary/ui/navigation/Screen.kt new file mode 100644 index 0000000..53f2c9e --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/navigation/Screen.kt @@ -0,0 +1,12 @@ +package com.hsdiary.ui.navigation + +sealed class Screen(val route: String) { + object Onboarding : Screen("onboarding") + object Calendar : Screen("calendar") + object DayDetail : Screen("day_detail/{date}") { + fun createRoute(date: String) = "day_detail/$date" + } + object CycleInsights : Screen("cycle_insights") + object HealthTrends : Screen("health_trends") + object Settings : Screen("settings") +} diff --git a/app/src/main/java/com/hsdiary/ui/onboarding/OnboardingScreen.kt b/app/src/main/java/com/hsdiary/ui/onboarding/OnboardingScreen.kt new file mode 100644 index 0000000..8155842 --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/onboarding/OnboardingScreen.kt @@ -0,0 +1,165 @@ +package com.hsdiary.ui.onboarding + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.hsdiary.data.model.ProfileType +import com.hsdiary.ui.theme.AvatarColors + +@Composable +fun OnboardingScreen( + onComplete: () -> Unit, + viewModel: OnboardingViewModel = hiltViewModel() +) { + val state by viewModel.uiState.collectAsState() + + when (state.step) { + 0 -> WelcomeStep(onNext = { viewModel.nextStep() }) + 1 -> ProfileSetupStep( + title = "Create your profile", + name = state.profile1Name, + colorIndex = state.profile1ColorIndex, + profileType = state.profile1Type, + onNameChange = viewModel::updateProfile1Name, + onColorChange = viewModel::updateProfile1Color, + onTypeChange = viewModel::updateProfile1Type, + onNext = { viewModel.nextStep() }, + canSkip = false + ) + 2 -> ProfileSetupStep( + title = "Add a second profile", + subtitle = "Optional — can be added later in Settings", + name = state.profile2Name, + colorIndex = state.profile2ColorIndex, + profileType = state.profile2Type, + onNameChange = viewModel::updateProfile2Name, + onColorChange = viewModel::updateProfile2Color, + onTypeChange = viewModel::updateProfile2Type, + onNext = { viewModel.completeOnboarding(onComplete) }, + canSkip = true, + onSkip = { viewModel.completeOnboarding(onComplete) }, + isLoading = state.isLoading + ) + } +} + +@Composable +private fun WelcomeStep(onNext: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize().padding(32.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("🩺", style = MaterialTheme.typography.displayLarge) + Spacer(Modifier.height(24.dp)) + Text( + text = "H&S Diary", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold + ) + Spacer(Modifier.height(12.dp)) + Text( + text = "Your private health & cycle tracker.\nAll data stays on your device.", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(48.dp)) + Button(onClick = onNext, modifier = Modifier.fillMaxWidth()) { + Text("Get Started") + } + } +} + +@Composable +private fun ProfileSetupStep( + title: String, + subtitle: String? = null, + name: String, + colorIndex: Int, + profileType: ProfileType, + onNameChange: (String) -> Unit, + onColorChange: (Int) -> Unit, + onTypeChange: (ProfileType) -> Unit, + onNext: () -> Unit, + canSkip: Boolean, + onSkip: (() -> Unit)? = null, + isLoading: Boolean = false +) { + Column( + modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp, vertical = 48.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Text(title, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) + if (subtitle != null) { + Text(subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + + OutlinedTextField( + value = name, + onValueChange = { if (it.length <= 32) onNameChange(it) }, + label = { Text("Name") }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + + // Profile type selector + Text("Profile type", style = MaterialTheme.typography.labelLarge) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + ProfileType.values().forEach { type -> + FilterChip( + selected = profileType == type, + onClick = { onTypeChange(type) }, + label = { Text(if (type == ProfileType.FEMALE) "Female" else "Male") } + ) + } + } + + // Color selector + Text("Avatar color", style = MaterialTheme.typography.labelLarge) + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + AvatarColors.forEachIndexed { idx, color -> + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(color) + .then( + if (idx == colorIndex) Modifier.border(3.dp, MaterialTheme.colorScheme.onSurface, CircleShape) + else Modifier + ) + .clickable { onColorChange(idx) } + ) + } + } + + Spacer(Modifier.weight(1f)) + + Button( + onClick = onNext, + enabled = !isLoading && (name.isNotBlank() || canSkip), + modifier = Modifier.fillMaxWidth() + ) { + if (isLoading) CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + else Text(if (canSkip && name.isBlank()) "Skip" else "Continue") + } + if (canSkip && name.isNotBlank()) { + TextButton(onClick = { onSkip?.invoke() }, modifier = Modifier.fillMaxWidth()) { + Text("Skip for now") + } + } + } +} diff --git a/app/src/main/java/com/hsdiary/ui/onboarding/OnboardingViewModel.kt b/app/src/main/java/com/hsdiary/ui/onboarding/OnboardingViewModel.kt new file mode 100644 index 0000000..8121f3f --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/onboarding/OnboardingViewModel.kt @@ -0,0 +1,77 @@ +package com.hsdiary.ui.onboarding + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hsdiary.data.db.entity.ProfileEntity +import com.hsdiary.data.model.ProfileType +import com.hsdiary.data.preferences.UserPreferences +import com.hsdiary.data.repository.ProfileRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class OnboardingUiState( + val step: Int = 0, // 0=welcome, 1=profile1, 2=profile2_prompt, 3=complete + val profile1Name: String = "", + val profile1ColorIndex: Int = 0, + val profile1Type: ProfileType = ProfileType.FEMALE, + val profile2Name: String = "", + val profile2ColorIndex: Int = 1, + val profile2Type: ProfileType = ProfileType.MALE, + val isLoading: Boolean = false +) + +@HiltViewModel +class OnboardingViewModel @Inject constructor( + private val profileRepository: ProfileRepository, + private val userPreferences: UserPreferences +) : ViewModel() { + + val onboardingComplete = userPreferences.onboardingComplete + + private val _uiState = MutableStateFlow(OnboardingUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun updateProfile1Name(name: String) { _uiState.value = _uiState.value.copy(profile1Name = name) } + fun updateProfile1Color(idx: Int) { _uiState.value = _uiState.value.copy(profile1ColorIndex = idx) } + fun updateProfile1Type(type: ProfileType) { _uiState.value = _uiState.value.copy(profile1Type = type) } + fun updateProfile2Name(name: String) { _uiState.value = _uiState.value.copy(profile2Name = name) } + fun updateProfile2Color(idx: Int) { _uiState.value = _uiState.value.copy(profile2ColorIndex = idx) } + fun updateProfile2Type(type: ProfileType) { _uiState.value = _uiState.value.copy(profile2Type = type) } + + fun nextStep() { _uiState.value = _uiState.value.copy(step = _uiState.value.step + 1) } + + fun completeOnboarding(onDone: () -> Unit) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true) + val state = _uiState.value + val colorHexes = listOf( + "#E91E63","#9C27B0","#2196F3","#009688", + "#4CAF50","#FF9800","#FF4081","#7C4DFF" + ) + val id1 = profileRepository.insertProfile( + ProfileEntity( + name = state.profile1Name.trim().ifEmpty { "Profile 1" }, + avatarColor = colorHexes[state.profile1ColorIndex], + profileType = state.profile1Type.name + ) + ) + userPreferences.setActiveProfileId(id1) + + if (state.profile2Name.isNotBlank()) { + profileRepository.insertProfile( + ProfileEntity( + name = state.profile2Name.trim(), + avatarColor = colorHexes[state.profile2ColorIndex], + profileType = state.profile2Type.name + ) + ) + } + userPreferences.setOnboardingComplete(true) + onDone() + } + } +} diff --git a/app/src/main/java/com/hsdiary/ui/settings/SettingsScreen.kt b/app/src/main/java/com/hsdiary/ui/settings/SettingsScreen.kt new file mode 100644 index 0000000..2697c35 --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/settings/SettingsScreen.kt @@ -0,0 +1,242 @@ +package com.hsdiary.ui.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.hsdiary.data.db.entity.ProfileEntity +import com.hsdiary.ui.calendar.parseColor +import com.hsdiary.ui.theme.AvatarColors + +private val colorHexes = listOf( + "#E91E63","#9C27B0","#2196F3","#009688", + "#4CAF50","#FF9800","#FF4081","#7C4DFF" +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen( + onBack: () -> Unit, + viewModel: SettingsViewModel = hiltViewModel() +) { + val state by viewModel.uiState.collectAsState() + var clearProfileTarget by remember { mutableStateOf(null) } + var showClearAll by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Settings") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") + } + } + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScroll(rememberScrollState()) + ) { + // App settings + SettingsSection("App") { + // First day of week + ListItem( + headlineContent = { Text("First day of week") }, + trailingContent = { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + FilterChip( + selected = state.firstDayOfWeek == 1, + onClick = { viewModel.setFirstDayOfWeek(1) }, + label = { Text("Sunday") } + ) + FilterChip( + selected = state.firstDayOfWeek == 2, + onClick = { viewModel.setFirstDayOfWeek(2) }, + label = { Text("Monday") } + ) + } + } + ) + HorizontalDivider() + // Theme + ListItem( + headlineContent = { Text("App theme") }, + trailingContent = { + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + listOf("Light", "Dark", "System").forEach { theme -> + val key = theme.uppercase() + FilterChip( + selected = state.appTheme == key, + onClick = { viewModel.setAppTheme(key) }, + label = { Text(theme) } + ) + } + } + } + ) + } + + // Profile settings + state.profiles.forEach { profile -> + SettingsSection("Profile: ${profile.name}") { + ProfileSettingsCard( + profile = profile, + onNameChange = { viewModel.updateProfileName(profile.id, it) }, + onColorChange = { viewModel.updateProfileColor(profile.id, it) } + ) + HorizontalDivider() + ListItem( + headlineContent = { Text("Clear profile data", color = MaterialTheme.colorScheme.error) }, + supportingContent = { Text("Removes all logs for this profile") }, + modifier = Modifier.clickable { clearProfileTarget = profile.id } + ) + } + } + + // Data management + SettingsSection("Data") { + ListItem( + headlineContent = { Text("Clear all data", color = MaterialTheme.colorScheme.error) }, + supportingContent = { Text("Removes all profiles and data — cannot be undone") }, + modifier = Modifier.clickable { showClearAll = true } + ) + } + + Spacer(Modifier.height(32.dp)) + Text( + "H&S Diary · All data stored locally on this device.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + } + + // Confirm clear profile + clearProfileTarget?.let { profileId -> + val profile = state.profiles.find { it.id == profileId } + AlertDialog( + onDismissRequest = { clearProfileTarget = null }, + title = { Text("Clear data?") }, + text = { Text("This will remove all logs for ${profile?.name}. This cannot be undone.") }, + confirmButton = { + TextButton(onClick = { + viewModel.clearProfileData(profileId) + clearProfileTarget = null + }, colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)) { + Text("Clear") + } + }, + dismissButton = { + TextButton(onClick = { clearProfileTarget = null }) { Text("Cancel") } + } + ) + } + + // Confirm clear all + if (showClearAll) { + AlertDialog( + onDismissRequest = { showClearAll = false }, + title = { Text("Clear all data?") }, + text = { Text("This will remove all profiles, logs, and reset the app. This CANNOT be undone.") }, + confirmButton = { + TextButton( + onClick = { viewModel.clearAllData(); showClearAll = false }, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) + ) { Text("Clear Everything") } + }, + dismissButton = { + TextButton(onClick = { showClearAll = false }) { Text("Cancel") } + } + ) + } +} + +@Composable +private fun SettingsSection(title: String, content: @Composable ColumnScope.() -> Unit) { + Column(modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) { + Text( + title, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + Card( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Column { content() } + } + } +} + +@Composable +private fun ProfileSettingsCard( + profile: ProfileEntity, + onNameChange: (String) -> Unit, + onColorChange: (String) -> Unit +) { + var editingName by remember(profile.id) { mutableStateOf(profile.name) } + var nameEditMode by remember { mutableStateOf(false) } + + Column(modifier = Modifier.padding(16.dp)) { + // Name + if (nameEditMode) { + OutlinedTextField( + value = editingName, + onValueChange = { if (it.length <= 32) editingName = it }, + label = { Text("Name") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + trailingIcon = { + TextButton(onClick = { + onNameChange(editingName) + nameEditMode = false + }) { Text("Save") } + } + ) + } else { + ListItem( + headlineContent = { Text(profile.name) }, + supportingContent = { Text("Tap to edit name") }, + modifier = Modifier.clickable { nameEditMode = true } + ) + } + + Spacer(Modifier.height(8.dp)) + Text("Avatar color", style = MaterialTheme.typography.labelMedium) + Spacer(Modifier.height(8.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(horizontal = 4.dp)) { + colorHexes.forEachIndexed { idx, hex -> + val color = parseColor(hex) + val isSelected = profile.avatarColor == hex + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(color) + .then(if (isSelected) Modifier.border(3.dp, MaterialTheme.colorScheme.onSurface, CircleShape) else Modifier) + .clickable { onColorChange(hex) } + ) + } + } + } +} diff --git a/app/src/main/java/com/hsdiary/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/hsdiary/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000..4d86a27 --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/settings/SettingsViewModel.kt @@ -0,0 +1,95 @@ +package com.hsdiary.ui.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hsdiary.data.db.entity.ProfileEntity +import com.hsdiary.data.model.ProfileType +import com.hsdiary.data.preferences.UserPreferences +import com.hsdiary.data.repository.CycleRepository +import com.hsdiary.data.repository.DayLogRepository +import com.hsdiary.data.repository.IntimacyRepository +import com.hsdiary.data.repository.ProfileRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class SettingsUiState( + val profiles: List = emptyList(), + val activeProfileId: Long? = null, + val firstDayOfWeek: Int = 1, + val appTheme: String = "SYSTEM", + val isLoading: Boolean = false, + val confirmClearProfileId: Long? = null, + val confirmClearAll: Boolean = false +) + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val profileRepository: ProfileRepository, + private val dayLogRepository: DayLogRepository, + private val cycleRepository: CycleRepository, + private val intimacyRepository: IntimacyRepository, + private val userPreferences: UserPreferences +) : ViewModel() { + + val uiState: StateFlow = combine( + profileRepository.getAllProfiles(), + userPreferences.activeProfileId, + userPreferences.firstDayOfWeek, + userPreferences.appTheme + ) { profiles, activeId, fdow, theme -> + SettingsUiState(profiles = profiles, activeProfileId = activeId, firstDayOfWeek = fdow, appTheme = theme) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), SettingsUiState()) + + fun setFirstDayOfWeek(day: Int) { + viewModelScope.launch { userPreferences.setFirstDayOfWeek(day) } + } + + fun setAppTheme(theme: String) { + viewModelScope.launch { userPreferences.setAppTheme(theme) } + } + + fun updateProfileName(profileId: Long, name: String) { + viewModelScope.launch { + val profile = profileRepository.getProfileById(profileId) ?: return@launch + profileRepository.updateProfile(profile.copy(name = name.trim())) + } + } + + fun updateProfileColor(profileId: Long, colorHex: String) { + viewModelScope.launch { + val profile = profileRepository.getProfileById(profileId) ?: return@launch + profileRepository.updateProfile(profile.copy(avatarColor = colorHex)) + } + } + + fun updateReproductiveStatus(profileId: Long, statusJson: String) { + viewModelScope.launch { + val profile = profileRepository.getProfileById(profileId) ?: return@launch + profileRepository.updateProfile(profile.copy(reproductiveStatus = statusJson)) + } + } + + fun clearProfileData(profileId: Long) { + viewModelScope.launch { + dayLogRepository.deleteAllForProfile(profileId) + cycleRepository.deleteAllForProfile(profileId) + intimacyRepository.deleteAllForProfile(profileId) + } + } + + fun clearAllData() { + viewModelScope.launch { + val profiles = profileRepository.getAllProfilesOnce() + profiles.forEach { p -> + dayLogRepository.deleteAllForProfile(p.id) + cycleRepository.deleteAllForProfile(p.id) + intimacyRepository.deleteAllForProfile(p.id) + profileRepository.deleteProfile(p) + } + userPreferences.setOnboardingComplete(false) + userPreferences.setActiveProfileId(0L) + } + } +} diff --git a/app/src/main/java/com/hsdiary/ui/theme/Color.kt b/app/src/main/java/com/hsdiary/ui/theme/Color.kt new file mode 100644 index 0000000..89d7eae --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/theme/Color.kt @@ -0,0 +1,31 @@ +package com.hsdiary.ui.theme + +import androidx.compose.ui.graphics.Color + +// Material theme seeds +val PrimaryPink = Color(0xFFE91E63) +val PrimaryPinkDark = Color(0xFFC2185B) +val OnPrimary = Color(0xFFFFFFFF) + +// Cycle phase colors +val PeriodColor = Color(0xFFB71C1C) +val PeriodPredictedColor = Color(0xFFEF9A9A) +val FertileColor = Color(0xFF00796B) +val FertilePredictedColor = Color(0xFF80CBC4) +val OvulationColor = Color(0xFF00897B) +val LutealColor = Color(0xFFF57F17) +val FollicularColor = Color(0xFFE0E0E0) + +// Avatar palette (8 swatches) +val AvatarColors = listOf( + Color(0xFFE91E63), // Rose + Color(0xFF9C27B0), // Purple + Color(0xFF2196F3), // Blue + Color(0xFF009688), // Teal + Color(0xFF4CAF50), // Green + Color(0xFFFF9800), // Orange + Color(0xFFFF4081), // Pink accent + Color(0xFF7C4DFF) // Violet +) + +val AvatarColorLabels = listOf("Rose", "Purple", "Blue", "Teal", "Green", "Orange", "Pink", "Violet") diff --git a/app/src/main/java/com/hsdiary/ui/theme/Theme.kt b/app/src/main/java/com/hsdiary/ui/theme/Theme.kt new file mode 100644 index 0000000..89465d6 --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/theme/Theme.kt @@ -0,0 +1,56 @@ +package com.hsdiary.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext + +private val LightColors = lightColorScheme( + primary = PrimaryPink, + onPrimary = OnPrimary, + primaryContainer = Color(0xFFFCE4EC), + onPrimaryContainer = Color(0xFF880E4F), + secondary = Color(0xFF009688), + onSecondary = OnPrimary, + secondaryContainer = Color(0xFFE0F2F1), + onSecondaryContainer = Color(0xFF004D40), + surface = Color(0xFFFFFBFE), + onSurface = Color(0xFF1C1B1F), + surfaceVariant = Color(0xFFF5F5F5), + onSurfaceVariant = Color(0xFF49454F), + outline = Color(0xFF79747E) +) + +private val DarkColors = darkColorScheme( + primary = Color(0xFFF48FB1), + onPrimary = Color(0xFF880E4F), + primaryContainer = Color(0xFFAD1457), + onPrimaryContainer = Color(0xFFFCE4EC), + secondary = Color(0xFF80CBC4), + onSecondary = Color(0xFF004D40), + surface = Color(0xFF1C1B1F), + onSurface = Color(0xFFE6E1E5) +) + +@Composable +fun HSDiaryTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColors + else -> LightColors + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/app/src/main/java/com/hsdiary/ui/theme/Type.kt b/app/src/main/java/com/hsdiary/ui/theme/Type.kt new file mode 100644 index 0000000..4a937c3 --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/theme/Type.kt @@ -0,0 +1,15 @@ +package com.hsdiary.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + bodyLarge = TextStyle(fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp), + bodyMedium = TextStyle(fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp), + bodySmall = TextStyle(fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 16.sp), + titleLarge = TextStyle(fontWeight = FontWeight.SemiBold, fontSize = 22.sp, lineHeight = 28.sp), + titleMedium = TextStyle(fontWeight = FontWeight.SemiBold, fontSize = 16.sp, lineHeight = 24.sp), + labelSmall = TextStyle(fontWeight = FontWeight.Medium, fontSize = 11.sp, lineHeight = 16.sp) +) diff --git a/app/src/main/java/com/hsdiary/ui/trends/HealthTrendsScreen.kt b/app/src/main/java/com/hsdiary/ui/trends/HealthTrendsScreen.kt new file mode 100644 index 0000000..ef556cb --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/trends/HealthTrendsScreen.kt @@ -0,0 +1,176 @@ +package com.hsdiary.ui.trends + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HealthTrendsScreen( + onBack: () -> Unit, + viewModel: HealthTrendsViewModel = hiltViewModel() +) { + val state by viewModel.uiState.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Health Trends") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") + } + } + ) + } + ) { padding -> + if (state.isLoading) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + return@Scaffold + } + + LazyColumn( + modifier = Modifier.fillMaxSize().padding(padding), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Range selector + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + TrendsRange.values().forEach { range -> + FilterChip( + selected = state.range == range, + onClick = { viewModel.setRange(range) }, + label = { + Text(when (range) { + TrendsRange.DAYS_30 -> "30d" + TrendsRange.MONTHS_3 -> "3m" + TrendsRange.MONTHS_6 -> "6m" + TrendsRange.ALL -> "All" + }, style = MaterialTheme.typography.labelSmall) + } + ) + } + } + } + + // Summary header + if (state.conditionFrequencies.isEmpty()) { + item { + Box( + Modifier.fillMaxWidth().padding(vertical = 48.dp), + contentAlignment = Alignment.Center + ) { + Text( + "No conditions logged in this range.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } else { + item { + Text( + "Logged this period", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + } + items(state.conditionFrequencies) { freq -> + ConditionFrequencyCard( + frequency = freq, + isSelected = state.selectedCondition?.conditionKey == freq.definition.conditionKey, + onClick = { + viewModel.selectCondition( + if (state.selectedCondition?.conditionKey == freq.definition.conditionKey) null + else freq.definition + ) + } + ) + } + } + } + } +} + +@Composable +private fun ConditionFrequencyCard( + frequency: ConditionFrequency, + isSelected: Boolean, + onClick: () -> Unit +) { + val barColor = MaterialTheme.colorScheme.primary + + ElevatedCard( + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(frequency.definition.displayName, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium) + if (frequency.isRecurring) { + Badge(containerColor = MaterialTheme.colorScheme.tertiaryContainer) { + Text("Recurring", style = MaterialTheme.typography.labelSmall) + } + } + } + Text( + "${frequency.count} times · avg rating ${"%.1f".format(frequency.avgRating)}/5", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + "×${frequency.count}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + } + + if (isSelected && frequency.weeklyData.isNotEmpty()) { + Spacer(Modifier.height(12.dp)) + Text("Weekly occurrences", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Spacer(Modifier.height(4.dp)) + val maxCount = (frequency.weeklyData.maxOrNull() ?: 1).toFloat().coerceAtLeast(1f) + Canvas(modifier = Modifier.fillMaxWidth().height(60.dp)) { + val barW = size.width / (frequency.weeklyData.size * 1.5f) + val gap = barW * 0.5f + frequency.weeklyData.forEachIndexed { i, count -> + val h = (count / maxCount) * size.height * 0.9f + drawRect( + color = barColor, + topLeft = Offset(i * (barW + gap), size.height - h), + size = Size(barW, h) + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/hsdiary/ui/trends/HealthTrendsViewModel.kt b/app/src/main/java/com/hsdiary/ui/trends/HealthTrendsViewModel.kt new file mode 100644 index 0000000..cef476a --- /dev/null +++ b/app/src/main/java/com/hsdiary/ui/trends/HealthTrendsViewModel.kt @@ -0,0 +1,130 @@ +package com.hsdiary.ui.trends + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hsdiary.data.db.entity.ConditionDefinitionEntity +import com.hsdiary.data.db.entity.ConditionEntryEntity +import com.hsdiary.data.db.entity.DayLogEntity +import com.hsdiary.data.db.entity.ProfileEntity +import com.hsdiary.data.model.ProfileType +import com.hsdiary.data.preferences.UserPreferences +import com.hsdiary.data.repository.CycleRepository +import com.hsdiary.data.repository.DayLogRepository +import com.hsdiary.data.repository.ProfileRepository +import com.hsdiary.domain.CyclePredictionEngine +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.time.LocalDate +import javax.inject.Inject + +enum class TrendsRange { DAYS_30, MONTHS_3, MONTHS_6, ALL } + +data class ConditionFrequency( + val definition: ConditionDefinitionEntity, + val count: Int, + val isRecurring: Boolean, + val avgRating: Float, + val weeklyData: List +) + +data class HealthTrendsUiState( + val profile: ProfileEntity? = null, + val range: TrendsRange = TrendsRange.DAYS_30, + val conditionFrequencies: List = emptyList(), + val selectedCondition: ConditionDefinitionEntity? = null, + val isLoading: Boolean = true, + val isFemale: Boolean = false +) + +@HiltViewModel +class HealthTrendsViewModel @Inject constructor( + private val profileRepository: ProfileRepository, + private val dayLogRepository: DayLogRepository, + private val userPreferences: UserPreferences +) : ViewModel() { + + private val _range = MutableStateFlow(TrendsRange.DAYS_30) + private val _selectedCondition = MutableStateFlow(null) + private val _uiState = MutableStateFlow(HealthTrendsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + viewModelScope.launch { load() } + } + + private suspend fun load() { + val activeId = userPreferences.activeProfileId.first() + val profiles = profileRepository.getAllProfilesOnce() + val profile = profiles.find { it.id == activeId } ?: profiles.firstOrNull() ?: return + val isFemale = profile.profileType == ProfileType.FEMALE.name + val allDefs = dayLogRepository.getAllDefinitions() + .filter { it.profileVisibility == "ALL" || (isFemale && it.profileVisibility == "FEMALE_ONLY") } + + _range.combine(_selectedCondition) { range, sel -> range to sel } + .onEach { (range, sel) -> + val today = LocalDate.now() + val startDate = when (range) { + TrendsRange.DAYS_30 -> today.minusDays(29) + TrendsRange.MONTHS_3 -> today.minusMonths(3) + TrendsRange.MONTHS_6 -> today.minusMonths(6) + TrendsRange.ALL -> today.minusYears(5) + } + + val entries = dayLogRepository.getConditionsInRange( + profile.id, startDate.toString(), today.toString() + ) + val dayLogs = dayLogRepository.getDayLogsInRangeOnce( + profile.id, startDate.toString(), today.toString() + ) + val dayLogMap = dayLogs.associateBy { it.id } + + val grouped = entries.groupBy { it.conditionKey } + val frequencies = allDefs.mapNotNull { def -> + val entriesForKey = grouped[def.conditionKey] ?: return@mapNotNull null + val weeklyData = buildWeeklyData(entriesForKey, dayLogMap, startDate, today) + ConditionFrequency( + definition = def, + count = entriesForKey.size, + isRecurring = entriesForKey.size >= 3, + avgRating = entriesForKey.map { it.rating }.average().toFloat(), + weeklyData = weeklyData + ) + }.sortedByDescending { it.count } + + _uiState.value = HealthTrendsUiState( + profile = profile, + range = range, + conditionFrequencies = frequencies, + selectedCondition = sel, + isLoading = false, + isFemale = isFemale + ) + } + .launchIn(viewModelScope) + } + + private fun buildWeeklyData( + entries: List, + dayLogMap: Map, + startDate: LocalDate, + today: LocalDate + ): List { + val weeks = mutableListOf() + var weekStart = startDate + while (!weekStart.isAfter(today)) { + val weekEnd = weekStart.plusDays(6) + val count = entries.count { entry -> + val log = dayLogMap[entry.dayLogId] ?: return@count false + val date = LocalDate.parse(log.date) + !date.isBefore(weekStart) && !date.isAfter(weekEnd) + } + weeks.add(count) + weekStart = weekStart.plusWeeks(1) + } + return weeks + } + + fun setRange(range: TrendsRange) { _range.value = range } + fun selectCondition(def: ConditionDefinitionEntity?) { _selectedCondition.value = def } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..dd3f185 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..6bf73b4 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6b78462 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6b78462 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..fd91a92 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + H&S Diary + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..9ace507 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +