diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d30cddb..db12478 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,8 @@ name: Build on: + push: + branches: [ develop ] pull_request: branches: [ main, develop ] @@ -14,10 +16,10 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - name: Make gradle executable run: chmod +x ./gradlew diff --git a/README.md b/README.md index e69de29..ce45df0 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,100 @@ +

MealTime

+ +# FocusBloom +FocusBloom is a Kotlin Multiplatform Pomodoro app that helps users enhance their productivity and time management skills through focused work intervals and short breaks. + +## Platforms +![](https://img.shields.io/badge/Android-black.svg?style=for-the-badge&logo=android) | ![](https://img.shields.io/badge/iOS-black.svg?style=for-the-badge&logo=apple) | ![](https://img.shields.io/badge/Desktop-black.svg?style=for-the-badge&logo=windows) | ![](https://img.shields.io/badge/Web-black.svg?style=for-the-badge&logo=google-chrome) +:----: | :----: | :----: | :----: +✅ | ✅ | ✅ | Planned +Get it on Google Play + +## Screenshots +### Android + + +### iOS + + +### Desktop + + + + + +## Architecture +The app is shared between Android, iOS and Desktop. The shared code is written in Kotlin and the UI is built with Compose Multiplatform. The shared code is compiled to Kotlin/JVM for Android and Kotlin/Native for iOS and Desktop. + + +### Modules +- shared: + - contains all the shared code between the platforms + - contains the business logic and data layer + - contains the UI layer + - contains the database layer + - contains the repository layer +- android: contains the android app + - contains the android app +- ios: contains the ios app + - contains the ios app +- desktop: contains the desktop app + - contains the desktop app + +## Built with +- [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) - The Kotlin Multiplatform technology is designed to simplify the development of cross-platform projects. +- [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/) - a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable. +- [SQLDelight](https://github.com/cashapp/sqldelight) - SQLDelight is an open-source library developed by Cash App (formerly Square, Inc.) for working with SQL databases in Kotlin-based Android and multi-platform applications. +- [Multiplatform Settings](https://github.com/russhwolf/multiplatform-settings) - A Kotlin Multiplatform library for saving simple key-value data. +- [Koin](https://insert-koin.io/) - The pragmatic Kotlin & Kotlin Multiplatform Dependency Injection framework. +- [Voyager](https://voyager.adriel.cafe/) - A multiplatform navigation library. +- [Kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) - KotlinX multiplatform date/time library. +- [Kotlinx-serilization](https://github.com/Kotlin/kotlinx.serialization) - Kotlin multiplatform / multi-format serialization. +- [Koala plot](https://github.com/KoalaPlot/koalaplot-core) - Koala Plot is a Compose Multiplatform based charting and plotting library written in Kotlin. +- [Compose Components Resources](https://mvnrepository.com/artifact/org.jetbrains.compose.components/components-resources) - Resources For Compose Multiplatform. +- [Material3 Window Size Multiplatform](https://github.com/chrisbanes/material3-windowsizeclass-multiplatform) - About Material 3 Window Size Class for Compose Multiplatform. +- [Spotless](https://github.com/diffplug/spotless) - A code formatter that helps keep the codebase clean. +- [Detekt](https://github.com/detekt/detekt) - Static code analysis for Kotlin. +- [Ktlint](https://github.com/pinterest/ktlint) - A static code analysis tool and formatter for Kotlin. +- [Github Actions](https://docs.github.com/en/actions) - A CI/CD tool that helps automate workflows. +- [Renovate](https://docs.renovatebot.com/) - An open-source software tool designed to help automate the process of updating dependencies in software projects. + +## Run project +### Android +To run the application on android device/emulator: +- open project in Android Studio and run imported android run configuration + +To build the application bundle: +- run `./gradlew :composeApp:assembleDebug` +- find `.apk` file in `composeApp/build/outputs/apk/debug/composeApp-debug.apk` + +### Desktop +Run the desktop application: `./gradlew :desktop:run` + +### iOS +To run the application on iPhone device/simulator: +- Open `ios/iosApp.xcworkspace` in Xcode and run standard configuration +- Or use [Kotlin Multiplatform Mobile plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile) for Android Studio + +## Todo +- [ ] Work on Notifications: To remind you of upcoming and overdue tasks +- [ ] Reminders: Sounds for breaks and work sessions +- [ ] +## Credits +- +## License +```xml +Copyright 2023 JoelKanyi + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +``` + diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 082b2d0..3e8edfd 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ plugins { alias(libs.plugins.android.application) alias(libs.plugins.android.kotlin) @@ -28,12 +43,12 @@ android { compileOptions { isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } buildFeatures { diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index ff61cc1..f6ebf7b 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -2,10 +2,11 @@ + + + + diff --git a/android/src/main/res/drawable/il_statistics.xml b/android/src/main/res/drawable/il_statistics.xml new file mode 100644 index 0000000..7032a4a --- /dev/null +++ b/android/src/main/res/drawable/il_statistics.xml @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..74685a1 Binary files /dev/null and b/android/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..4fafcd9 Binary files /dev/null and b/android/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..4337f1d Binary files /dev/null and b/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..2e70dcf Binary files /dev/null and b/android/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..a96a7f6 Binary files /dev/null and b/android/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..c496210 Binary files /dev/null and b/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..3cb4f58 Binary files /dev/null and b/android/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..353aa1a Binary files /dev/null and b/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..cee7deb Binary files /dev/null and b/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..a77d64f Binary files /dev/null and b/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..f93bb03 Binary files /dev/null and b/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..fe2c15e Binary files /dev/null and b/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..55dd818 Binary files /dev/null and b/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..716f7e4 Binary files /dev/null and b/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..8c6ea7a Binary files /dev/null and b/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/src/main/res/values/ic_launcher_background.xml b/android/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/android/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/art/app_logo.png b/art/app_logo.png new file mode 100644 index 0000000..4576095 Binary files /dev/null and b/art/app_logo.png differ diff --git a/art/app_screen1.jpeg b/art/app_screen1.jpeg new file mode 100644 index 0000000..eed9087 Binary files /dev/null and b/art/app_screen1.jpeg differ diff --git a/art/app_screen2.jpeg b/art/app_screen2.jpeg new file mode 100644 index 0000000..b0f279d Binary files /dev/null and b/art/app_screen2.jpeg differ diff --git a/art/app_screen3.jpeg b/art/app_screen3.jpeg new file mode 100644 index 0000000..b5c2f03 Binary files /dev/null and b/art/app_screen3.jpeg differ diff --git a/art/app_screen4.jpeg b/art/app_screen4.jpeg new file mode 100644 index 0000000..fa13687 Binary files /dev/null and b/art/app_screen4.jpeg differ diff --git a/art/app_screen5.jpeg b/art/app_screen5.jpeg new file mode 100644 index 0000000..9bce133 Binary files /dev/null and b/art/app_screen5.jpeg differ diff --git a/art/dsk_screen1.png b/art/dsk_screen1.png new file mode 100644 index 0000000..d52bcb0 Binary files /dev/null and b/art/dsk_screen1.png differ diff --git a/art/dsk_screen2.png b/art/dsk_screen2.png new file mode 100644 index 0000000..164478d Binary files /dev/null and b/art/dsk_screen2.png differ diff --git a/art/dsk_screen3.png b/art/dsk_screen3.png new file mode 100644 index 0000000..a2d9d1e Binary files /dev/null and b/art/dsk_screen3.png differ diff --git a/art/dsk_screen4.png b/art/dsk_screen4.png new file mode 100644 index 0000000..103de3f Binary files /dev/null and b/art/dsk_screen4.png differ diff --git a/art/dsk_screen5.png b/art/dsk_screen5.png new file mode 100644 index 0000000..4b42d1c Binary files /dev/null and b/art/dsk_screen5.png differ diff --git a/art/ios_screen1.png b/art/ios_screen1.png new file mode 100644 index 0000000..17f92a4 Binary files /dev/null and b/art/ios_screen1.png differ diff --git a/art/ios_screen2.png b/art/ios_screen2.png new file mode 100644 index 0000000..1d8aa1d Binary files /dev/null and b/art/ios_screen2.png differ diff --git a/art/ios_screen3.png b/art/ios_screen3.png new file mode 100644 index 0000000..efd6aaa Binary files /dev/null and b/art/ios_screen3.png differ diff --git a/art/ios_screen4.png b/art/ios_screen4.png new file mode 100644 index 0000000..cce5a36 Binary files /dev/null and b/art/ios_screen4.png differ diff --git a/art/ios_screen5.png b/art/ios_screen5.png new file mode 100644 index 0000000..83e3e7f Binary files /dev/null and b/art/ios_screen5.png differ diff --git a/art/ios_screen6.png b/art/ios_screen6.png new file mode 100644 index 0000000..b325c68 Binary files /dev/null and b/art/ios_screen6.png differ diff --git a/art/ios_screen7.png b/art/ios_screen7.png new file mode 100644 index 0000000..08e803e Binary files /dev/null and b/art/ios_screen7.png differ diff --git a/art/ios_screen8.png b/art/ios_screen8.png new file mode 100644 index 0000000..babfe9d Binary files /dev/null and b/art/ios_screen8.png differ diff --git a/art/ios_screen9.png b/art/ios_screen9.png new file mode 100644 index 0000000..30a828c Binary files /dev/null and b/art/ios_screen9.png differ diff --git a/build.gradle.kts b/build.gradle.kts index 2360ecb..445f22e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,11 +6,10 @@ plugins { alias(libs.plugins.jvm) apply false alias(libs.plugins.nativeCocoapod) apply false alias(libs.plugins.compose.multiplatform) - - alias(libs.plugins.ktLint) + alias(libs.plugins.spotless) + alias(libs.plugins.ktlint) alias(libs.plugins.detekt) alias(libs.plugins.gradleVersionUpdates) - } allprojects { @@ -18,6 +17,7 @@ allprojects { google() mavenCentral() maven(url = "https://jitpack.io") + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } apply(plugin = "org.jlleitschuh.gradle.ktlint") @@ -48,8 +48,29 @@ subprojects { outputDir = "build/reports/dependencyUpdates" reportfileName = "report" } -} -tasks.register("clean", Delete::class) { - delete(rootProject.buildDir) + apply(plugin = "com.diffplug.spotless") + spotless { + kotlin { + target("**/*.kt") + ktlint().userData(mapOf("disabled_rules" to "filename")) + licenseHeaderFile( + rootProject.file("${project.rootDir}/spotless/copyright.kt"), + "^(package|object|import|interface)" + ) + trimTrailingWhitespace() + endWithNewline() + } + format("kts") { + target("**/*.kts") + targetExclude("$buildDir/**/*.kts") + licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") + } + format("misc") { + target("**/*.md", "**/.gitignore") + trimTrailingWhitespace() + indentWithTabs() + endWithNewline() + } + } } diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts deleted file mode 100644 index bf37d31..0000000 --- a/core/common/build.gradle.kts +++ /dev/null @@ -1,80 +0,0 @@ -plugins { - alias(libs.plugins.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.kotlinX.serialization.plugin) - alias(libs.plugins.sqlDelight.plugin) -} - -android { - namespace = "com.joelkanyi.focusbloom.common" - compileSdk = 34 - defaultConfig { - minSdk = 21 - } -} - -kotlin { - jvm() - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "1.8" - } - } - } - iosX64() - iosArm64() - iosSimulatorArm64() - - sourceSets { - val commonMain by getting { - dependencies { - api(libs.koin.core) - - implementation(libs.kotlinX.serializationJson) - - implementation(libs.sqlDelight.runtime) - implementation(libs.sqlDelight.coroutine) - - implementation(libs.multiplatformSettings.noArg) - implementation(libs.multiplatformSettings.coroutines) - - api(libs.napier) - - implementation(libs.kotlinX.dateTime) - } - } - - val commonTest by getting { - dependencies { - dependsOn(commonMain) - implementation(libs.kotlin.test) - } - } - - val androidMain by getting { - dependencies { - implementation(libs.sqlDelight.android) - } - } - - val jvmMain by getting { - dependencies { - implementation(libs.sqlDelight.jvm) - } - } - - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(commonMain) - iosArm64Main.dependsOn(this) - iosX64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - dependencies { - // implementation(libs.sqlDelight.native) - } - } - } -} \ No newline at end of file diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts deleted file mode 100644 index 30d75ef..0000000 --- a/core/designsystem/build.gradle.kts +++ /dev/null @@ -1,64 +0,0 @@ -plugins { - alias(libs.plugins.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.kotlinX.serialization.plugin) - alias(libs.plugins.compose.multiplatform) -} - -android { - namespace = "com.joelkanyi.focusbloom.designsystem" - compileSdk = 34 - defaultConfig { - minSdk = 21 - } -} - -kotlin { - jvm() - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "1.8" - } - } - } - iosX64() - iosArm64() - iosSimulatorArm64() - - sourceSets { - val commonMain by getting { - dependencies { - implementation(compose.material3) - } - } - - val commonTest by getting { - dependencies { - dependsOn(commonMain) - } - } - - val androidMain by getting { - dependencies { - } - } - - val jvmMain by getting { - dependencies { - } - } - - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(commonMain) - iosArm64Main.dependsOn(this) - iosX64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - dependencies { - } - } - } -} diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 46942ec..6072b87 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ plugins { alias(libs.plugins.jvm) alias(libs.plugins.compose.multiplatform) @@ -12,7 +27,12 @@ compose.desktop { application { mainClass = "DesktopAppKt" nativeDistributions { - targetFormats(org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg, org.jetbrains.compose.desktop.application.dsl.TargetFormat.Msi, org.jetbrains.compose.desktop.application.dsl.TargetFormat.Deb) + targetFormats( + org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg, + org.jetbrains.compose.desktop.application.dsl.TargetFormat.Msi, + org.jetbrains.compose.desktop.application.dsl.TargetFormat.Deb, + org.jetbrains.compose.desktop.application.dsl.TargetFormat.Rpm + ) packageName = "focusbloom" packageName = "1.0.0" } diff --git a/desktop/src/main/kotlin/DesktopApp.kt b/desktop/src/main/kotlin/DesktopApp.kt index 9111828..1a07857 100644 --- a/desktop/src/main/kotlin/DesktopApp.kt +++ b/desktop/src/main/kotlin/DesktopApp.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Alignment @@ -8,15 +23,15 @@ import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import com.joelkanyi.focusbloom.FocusBloomApp -import com.joelkanyi.focusbloom.di.initKoin +import com.joelkanyi.focusbloom.di.KoinInit import org.koin.core.Koin lateinit var koin: Koin fun main() { - koin = initKoin().koin + koin = KoinInit().init() koin.loadModules( - listOf(), + listOf() ) return application { @@ -26,8 +41,8 @@ fun main() { state = rememberWindowState( position = WindowPosition.Aligned(Alignment.Center), width = 1200.dp, - height = 700.dp, - ), + height = 700.dp + ) ) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { FocusBloomApp() diff --git a/feature/calendar/build.gradle.kts b/feature/calendar/build.gradle.kts deleted file mode 100644 index 52a412b..0000000 --- a/feature/calendar/build.gradle.kts +++ /dev/null @@ -1,80 +0,0 @@ -plugins { - alias(libs.plugins.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.kotlinX.serialization.plugin) - alias(libs.plugins.sqlDelight.plugin) -} - -android { - namespace = "com.joelkanyi.focusbloom.calendar" - compileSdk = 34 - defaultConfig { - minSdk = 21 - } -} - -kotlin { - jvm() - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "1.8" - } - } - } - iosX64() - iosArm64() - iosSimulatorArm64() - - sourceSets { - val commonMain by getting { - dependencies { - api(libs.koin.core) - - implementation(libs.kotlinX.serializationJson) - - implementation(libs.sqlDelight.runtime) - implementation(libs.sqlDelight.coroutine) - - implementation(libs.multiplatformSettings.noArg) - implementation(libs.multiplatformSettings.coroutines) - - api(libs.napier) - - implementation(libs.kotlinX.dateTime) - } - } - - val commonTest by getting { - dependencies { - dependsOn(commonMain) - implementation(libs.kotlin.test) - } - } - - val androidMain by getting { - dependencies { - implementation(libs.sqlDelight.android) - } - } - - val jvmMain by getting { - dependencies { - implementation(libs.sqlDelight.jvm) - } - } - - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(commonMain) - iosArm64Main.dependsOn(this) - iosX64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - dependencies { - // implementation(libs.sqlDelight.native) - } - } - } -} \ No newline at end of file diff --git a/feature/calendar/src/commonMain/kotlin/com/joelkanyi/focusbloom/Calendar.kt b/feature/calendar/src/commonMain/kotlin/com/joelkanyi/focusbloom/Calendar.kt deleted file mode 100644 index 67a9f6d..0000000 --- a/feature/calendar/src/commonMain/kotlin/com/joelkanyi/focusbloom/Calendar.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.joelkanyi.focusbloom - -class Calendar { - fun getCalendar(): String { - return "Testing Calendar from ViewModel" - } -} diff --git a/feature/calendar/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/CalendarModule.kt b/feature/calendar/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/CalendarModule.kt deleted file mode 100644 index 43da273..0000000 --- a/feature/calendar/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/CalendarModule.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.joelkanyi.focusbloom.di - -import com.joelkanyi.focusbloom.Calendar -import com.joelkanyi.focusbloom.presentation.CalendarViewModel -import org.koin.dsl.module - -val calendarModule = module { - single { Calendar() } - single { CalendarViewModel(calendar = get()) } -} diff --git a/feature/calendar/src/commonMain/kotlin/com/joelkanyi/focusbloom/presentation/CalendarViewModel.kt b/feature/calendar/src/commonMain/kotlin/com/joelkanyi/focusbloom/presentation/CalendarViewModel.kt deleted file mode 100644 index 141a683..0000000 --- a/feature/calendar/src/commonMain/kotlin/com/joelkanyi/focusbloom/presentation/CalendarViewModel.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.joelkanyi.focusbloom.presentation - -import com.joelkanyi.focusbloom.Calendar -import org.koin.core.component.KoinComponent - -class CalendarViewModel( - private val calendar: Calendar, -) : KoinComponent { - fun getCalendar() = calendar.getCalendar() -} diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts deleted file mode 100644 index fc5b78e..0000000 --- a/feature/home/build.gradle.kts +++ /dev/null @@ -1,122 +0,0 @@ -plugins { - alias(libs.plugins.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.kotlinX.serialization.plugin) - alias(libs.plugins.nativeCocoapod) - alias(libs.plugins.sqlDelight.plugin) - alias(libs.plugins.compose.multiplatform) -} - -android { - namespace = "com.joelkanyi.focusbloom.home" - sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") - sourceSets["main"].res.srcDirs("src/commonMain/resources") - sourceSets["main"].resources.srcDirs("src/commonMain/resources") - compileSdk = 34 - defaultConfig { - minSdk = 21 - } -} -kotlin { - jvm() - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "1.8" - } - } - } - iosX64() - iosArm64() - iosSimulatorArm64() - - cocoapods { - version = "1.0.0" - summary = "Some description for the Shared Module" - homepage = "Link to the Shared Module homepage" - ios.deploymentTarget = "14.1" - podfile = project.file("../../ios/Podfile") - framework { - baseName = "home" - isStatic = true - } - } - - sourceSets { - val commonMain by getting { - dependencies { - api(project(":core:common")) - api(project(":feature:settings")) - api(project(":feature:statistics")) - api(project(":feature:calendar")) - api(libs.koin.core) - - implementation(compose.material3) - implementation(compose.material) - implementation(compose.materialIconsExtended) - - @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) - implementation(compose.components.resources) - - implementation(libs.voyager.navigator) - implementation(libs.voyager.bottomSheetNavigator) - implementation(libs.voyager.transitions) - implementation(libs.voyager.tabNavigator) - // implementation(libs.voyager.koin) - - // api(libs.ktor.core) - // api(libs.ktor.cio) - // implementation(libs.ktor.contentNegotiation) - // implementation(libs.ktor.json) - // implementation(libs.ktor.logging) - - implementation(libs.kotlinX.serializationJson) - - implementation("dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.3.1") - - implementation(libs.sqlDelight.runtime) - implementation(libs.sqlDelight.coroutine) - - implementation(libs.multiplatformSettings.noArg) - implementation(libs.multiplatformSettings.coroutines) - - api(libs.napier) - - implementation(libs.kotlinX.dateTime) - } - } - - val commonTest by getting { - dependencies { - dependsOn(commonMain) - implementation(libs.kotlin.test) - } - } - - val androidMain by getting { - dependencies { - implementation(libs.sqlDelight.android) - } - } - - val jvmMain by getting { - dependencies { - implementation(libs.sqlDelight.jvm) - } - } - - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(commonMain) - iosArm64Main.dependsOn(this) - iosX64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - dependencies { - // implementation(libs.sqlDelight.native) - implementation(libs.components.resources) - } - } - } -} \ No newline at end of file diff --git a/feature/home/src/commonMain/kotlin/com.joelkanyi.focusbloom/Home.kt b/feature/home/src/commonMain/kotlin/com.joelkanyi.focusbloom/Home.kt deleted file mode 100644 index 2b58764..0000000 --- a/feature/home/src/commonMain/kotlin/com.joelkanyi.focusbloom/Home.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.joelkanyi.focusbloom - -class Home { - fun getHome(): String { - return "Testing Home from ViewModel" - } -} diff --git a/feature/home/src/commonMain/kotlin/com.joelkanyi.focusbloom/di/HomeModule.kt b/feature/home/src/commonMain/kotlin/com.joelkanyi.focusbloom/di/HomeModule.kt deleted file mode 100644 index 4c63308..0000000 --- a/feature/home/src/commonMain/kotlin/com.joelkanyi.focusbloom/di/HomeModule.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.joelkanyi.focusbloom.di - -import com.joelkanyi.focusbloom.Home -import com.joelkanyi.focusbloom.presentation.HomeViewModel -import org.koin.dsl.module - -val homeModule = module { - single { Home() } - single { HomeViewModel(home = get()) } -} diff --git a/feature/home/src/commonMain/kotlin/com.joelkanyi.focusbloom/presentation/HomeViewModel.kt b/feature/home/src/commonMain/kotlin/com.joelkanyi.focusbloom/presentation/HomeViewModel.kt deleted file mode 100644 index 8998aca..0000000 --- a/feature/home/src/commonMain/kotlin/com.joelkanyi.focusbloom/presentation/HomeViewModel.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.joelkanyi.focusbloom.presentation - -import com.joelkanyi.focusbloom.Home -import org.koin.core.component.KoinComponent - -class HomeViewModel(private val home: Home) : KoinComponent { - - fun getHome(): String { - return home.getHome() - } -} diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts deleted file mode 100644 index 7ccbee2..0000000 --- a/feature/settings/build.gradle.kts +++ /dev/null @@ -1,80 +0,0 @@ -plugins { - alias(libs.plugins.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.kotlinX.serialization.plugin) - alias(libs.plugins.sqlDelight.plugin) -} - -android { - namespace = "com.joelkanyi.focusbloom.settings" - compileSdk = 34 - defaultConfig { - minSdk = 21 - } -} - -kotlin { - jvm() - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "1.8" - } - } - } - iosX64() - iosArm64() - iosSimulatorArm64() - - sourceSets { - val commonMain by getting { - dependencies { - api(libs.koin.core) - - implementation(libs.kotlinX.serializationJson) - - implementation(libs.sqlDelight.runtime) - implementation(libs.sqlDelight.coroutine) - - implementation(libs.multiplatformSettings.noArg) - implementation(libs.multiplatformSettings.coroutines) - - api(libs.napier) - - implementation(libs.kotlinX.dateTime) - } - } - - val commonTest by getting { - dependencies { - dependsOn(commonMain) - implementation(libs.kotlin.test) - } - } - - val androidMain by getting { - dependencies { - implementation(libs.sqlDelight.android) - } - } - - val jvmMain by getting { - dependencies { - implementation(libs.sqlDelight.jvm) - } - } - - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(commonMain) - iosArm64Main.dependsOn(this) - iosX64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - dependencies { - // implementation(libs.sqlDelight.native) - } - } - } -} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/joelkanyi/focusbloom/Settings.kt b/feature/settings/src/commonMain/kotlin/com/joelkanyi/focusbloom/Settings.kt deleted file mode 100644 index 9e94768..0000000 --- a/feature/settings/src/commonMain/kotlin/com/joelkanyi/focusbloom/Settings.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.joelkanyi.focusbloom - -class Settings { - fun getSettings(): String { - return "Testing Settings from ViewModel" - } -} diff --git a/feature/settings/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/SettingsModule.kt b/feature/settings/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/SettingsModule.kt deleted file mode 100644 index 1993ad4..0000000 --- a/feature/settings/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/SettingsModule.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.joelkanyi.focusbloom.di - -import com.joelkanyi.focusbloom.Settings -import com.joelkanyi.focusbloom.presentation.SettingsViewModel -import org.koin.dsl.module - -val settingsModule = module { - single { Settings() } - single { SettingsViewModel(settings = get()) } -} diff --git a/feature/settings/src/commonMain/kotlin/com/joelkanyi/focusbloom/presentation/SettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/com/joelkanyi/focusbloom/presentation/SettingsViewModel.kt deleted file mode 100644 index 57a36ad..0000000 --- a/feature/settings/src/commonMain/kotlin/com/joelkanyi/focusbloom/presentation/SettingsViewModel.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.joelkanyi.focusbloom.presentation - -import com.joelkanyi.focusbloom.Settings -import org.koin.core.component.KoinComponent - -class SettingsViewModel( - private val settings: Settings, -) : KoinComponent { - fun getSettings() = settings.getSettings() -} diff --git a/feature/statistics/build.gradle.kts b/feature/statistics/build.gradle.kts deleted file mode 100644 index 73d8f9d..0000000 --- a/feature/statistics/build.gradle.kts +++ /dev/null @@ -1,80 +0,0 @@ -plugins { - alias(libs.plugins.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.kotlinX.serialization.plugin) - alias(libs.plugins.sqlDelight.plugin) -} - -android { - namespace = "com.joelkanyi.focusbloom.statistics" - compileSdk = 34 - defaultConfig { - minSdk = 21 - } -} - -kotlin { - jvm() - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "1.8" - } - } - } - iosX64() - iosArm64() - iosSimulatorArm64() - - sourceSets { - val commonMain by getting { - dependencies { - api(libs.koin.core) - - implementation(libs.kotlinX.serializationJson) - - implementation(libs.sqlDelight.runtime) - implementation(libs.sqlDelight.coroutine) - - implementation(libs.multiplatformSettings.noArg) - implementation(libs.multiplatformSettings.coroutines) - - api(libs.napier) - - implementation(libs.kotlinX.dateTime) - } - } - - val commonTest by getting { - dependencies { - dependsOn(commonMain) - implementation(libs.kotlin.test) - } - } - - val androidMain by getting { - dependencies { - implementation(libs.sqlDelight.android) - } - } - - val jvmMain by getting { - dependencies { - implementation(libs.sqlDelight.jvm) - } - } - - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(commonMain) - iosArm64Main.dependsOn(this) - iosX64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - dependencies { - // implementation(libs.sqlDelight.native) - } - } - } -} \ No newline at end of file diff --git a/feature/statistics/src/commonMain/kotlin/com.joelkanyi.focusbloom/Statistics.kt b/feature/statistics/src/commonMain/kotlin/com.joelkanyi.focusbloom/Statistics.kt deleted file mode 100644 index d1657d3..0000000 --- a/feature/statistics/src/commonMain/kotlin/com.joelkanyi.focusbloom/Statistics.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.joelkanyi.focusbloom - -class Statistics { - fun getStatistics(): String { - return "Testing Statistics from ViewModel" - } -} diff --git a/feature/statistics/src/commonMain/kotlin/com.joelkanyi.focusbloom/di/StatisticsModule.kt b/feature/statistics/src/commonMain/kotlin/com.joelkanyi.focusbloom/di/StatisticsModule.kt deleted file mode 100644 index 279ede4..0000000 --- a/feature/statistics/src/commonMain/kotlin/com.joelkanyi.focusbloom/di/StatisticsModule.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.joelkanyi.focusbloom.di - -import com.joelkanyi.focusbloom.Statistics -import com.joelkanyi.focusbloom.presentation.StatisticsViewModel -import org.koin.dsl.module - -val statisticsModule = module { - single { Statistics() } - single { StatisticsViewModel(statistics = get()) } -} diff --git a/feature/statistics/src/commonMain/kotlin/com.joelkanyi.focusbloom/presentation/StatisticsViewModel.kt b/feature/statistics/src/commonMain/kotlin/com.joelkanyi.focusbloom/presentation/StatisticsViewModel.kt deleted file mode 100644 index d16bb13..0000000 --- a/feature/statistics/src/commonMain/kotlin/com.joelkanyi.focusbloom/presentation/StatisticsViewModel.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.joelkanyi.focusbloom.presentation - -import com.joelkanyi.focusbloom.Statistics -import org.koin.core.component.KoinComponent - -class StatisticsViewModel( - private val statistics: Statistics, -) : KoinComponent { - fun getStatistics() = statistics.getStatistics() -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c03472..9936550 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,25 +1,31 @@ [versions] -componentsResources = "1.5.0" -kotlin = "1.9.0" -gradle = "8.1.0" -ktLint = "10.3.0" +componentsResources = "1.5.1" +koalaplotCore = "0.4.0-dev1" +korau = "4.0.6" +kotlin = "1.9.10" +gradle = "8.1.1" +kotlinxCoroutinesSwing = "1.7.3" detekt = "1.19.0" gradleVersionUpdate = "0.46.0" -koinCore = "3.4.0" -koinAndroid = "3.4.0" -kotlinxSerializationJson = "1.5.0" -kotlinxDateTime = "0.4.0" +koinCore = "3.4.3" +koinAndroid = "3.4.3" +kotlinxSerializationJson = "1.6.0" +kotlinxDateTime = "0.4.1" material3WindowSizeClassMultiplatform = "0.3.1" -napier = "2.4.0" +napier = "2.6.1" +primitiveAdapters = "2.0.0" sqlDelight = "1.5.5" -multiplatformSettings = "1.0.0" -compose = "1.5.0" -core-library-desugaring = "1.1.5" -voyager = "1.0.0-rc06" +multiplatformSettings = "0.8.1" +compose = "1.5.10-beta01" +core-library-desugaring = "2.0.3" +voyager = "1.0.0-rc07" compose-activity = "1.7.2" +koin-compose = "1.0.4" +spotless = "5.17.1" +ktlint = "11.3.1" +accompanist-systemUIController = "0.30.1" [plugins] -ktLint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktLint" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose" } gradleVersionUpdates = { id = "com.github.ben-manes.versions", version.ref = "gradleVersionUpdate" } @@ -31,12 +37,19 @@ android-application = { id = "com.android.application", version.ref = "gradle" } jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinX-serialization-plugin = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } sqlDelight-plugin = { id = "com.squareup.sqldelight", version.ref = "sqlDelight" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } [libraries] components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "componentsResources" } +koalaplot-core = { module = "io.github.koalaplot:koalaplot-core", version.ref = "koalaplotCore" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinCore" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin-compose"} +korau = { module = "com.soywiz.korlibs.korau:korau", version.ref = "korau" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesSwing" } kotlinX-serializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationJson" } material3-window-size-multiplatform = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "material3WindowSizeClassMultiplatform" } +primitive-adapters = { module = "app.cash.sqldelight:primitive-adapters", version.ref = "primitiveAdapters" } sqlDelight-runtime = { module = "com.squareup.sqldelight:runtime", version.ref = "sqlDelight" } sqlDelight-coroutine = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqlDelight" } sqlDelight-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqlDelight" } @@ -54,3 +67,4 @@ voyager-bottomSheetNavigator = { module = "cafe.adriel.voyager:voyager-bottom-sh voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } core-library-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "core-library-desugaring" } +accompanist-systemUIController = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist-systemUIController" } \ No newline at end of file diff --git a/ios/Pods/Pods.xcodeproj/project.pbxproj b/ios/Pods/Pods.xcodeproj/project.pbxproj index d9c6621..759202a 100644 --- a/ios/Pods/Pods.xcodeproj/project.pbxproj +++ b/ios/Pods/Pods.xcodeproj/project.pbxproj @@ -9,9 +9,10 @@ /* Begin PBXAggregateTarget section */ 8777C9F6889E59EFFD631D80AEE9048B /* shared */ = { isa = PBXAggregateTarget; - buildConfigurationList = 8349D8E2EC974421A14EF8ABFF6AD6DC /* Build configuration list for PBXAggregateTarget "shared" */; + buildConfigurationList = B999BD58A146737F72CA68509B0BCE77 /* Build configuration list for PBXAggregateTarget "shared" */; buildPhases = ( - BEA8885189D408D600647BDC228A6A20 /* [CP-User] Build shared */, + 11F372A9E68C08375C21A8D47B3CDB02 /* [CP-User] Build shared */, + 35C7D8836447FEC746DA683E14000153 /* [CP] Copy dSYMs */, ); dependencies = ( ); @@ -21,8 +22,8 @@ /* Begin PBXBuildFile section */ 648F16425FEF89525AE0325F5A984B86 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */; }; - 8749C8E8DC500B064FA0BC7A78C38A2A /* Pods-iosApp-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BC3BD8CAFAE0C8EB92CD04E5FC24E61 /* Pods-iosApp-dummy.m */; }; - 8801CBFD38B946597BD07145B2EEFC9F /* Pods-iosApp-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 70E8DFC7821955063C886C71258CBE53 /* Pods-iosApp-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8749C8E8DC500B064FA0BC7A78C38A2A /* Pods-iosApp-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = E6DB2A5F5DADA1DDE45F36B1A2D6AC16 /* Pods-iosApp-dummy.m */; }; + 8801CBFD38B946597BD07145B2EEFC9F /* Pods-iosApp-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 015E0D7EA7331961AB63E5AFECA86BB5 /* Pods-iosApp-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -36,23 +37,25 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 2310A0FF4C69C2DB4993B8D41A756CB4 /* shared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = shared.framework; path = build/cocoapods/framework/shared.framework; sourceTree = ""; }; - 257390D34074D2442461A69FE6970CBD /* Pods-iosApp-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-iosApp-resources.sh"; sourceTree = ""; }; - 293144EFA3ECD0A4AF2FCAD935435823 /* shared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = shared.release.xcconfig; sourceTree = ""; }; - 433D35B85989957C756599EEE9AA3B8A /* compose-resources */ = {isa = PBXFileReference; includeInIndex = 1; name = "compose-resources"; path = "build/compose/ios/shared/compose-resources"; sourceTree = ""; }; - 4D3E6DCB9CAB65A8A05C467E2BBC1F0D /* Pods-iosApp-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-iosApp-acknowledgements.markdown"; sourceTree = ""; }; - 6A3C5EB0586A09C512019B6B6A2DE103 /* Pods-iosApp-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iosApp-Info.plist"; sourceTree = ""; }; - 70E8DFC7821955063C886C71258CBE53 /* Pods-iosApp-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-iosApp-umbrella.h"; sourceTree = ""; }; + 015E0D7EA7331961AB63E5AFECA86BB5 /* Pods-iosApp-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-iosApp-umbrella.h"; sourceTree = ""; }; + 11C9B998CE0869936AE6BE69270DAAC9 /* Pods-iosApp.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-iosApp.modulemap"; sourceTree = ""; }; + 15C62982CC8B1FD56214604CE982F646 /* compose-resources */ = {isa = PBXFileReference; includeInIndex = 1; name = "compose-resources"; path = "build/compose/ios/shared/compose-resources"; sourceTree = ""; }; + 215D65FA831B0E4F8AC0B3826A846FDA /* shared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = shared.debug.xcconfig; sourceTree = ""; }; + 244FC2DAA936A0B07ACEFDC74E2D54B8 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-iosApp.release.xcconfig"; sourceTree = ""; }; + 45CF8D032A049A7012F30257A8F121AF /* shared-copy-dsyms.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "shared-copy-dsyms.sh"; sourceTree = ""; }; + 55ABB06C8A1800962A74E007E7733796 /* Pods-iosApp-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-iosApp-frameworks.sh"; sourceTree = ""; }; + 5F931FED0EB9A618FB9EFF402884E1AC /* shared.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = shared.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 670597F16E31933FDD530ED5140D4EEF /* shared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = shared.framework; path = build/cocoapods/framework/shared.framework; sourceTree = ""; }; + 6F3B5FED07117E377CFAC3D49B490F17 /* Pods-iosApp-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-iosApp-resources.sh"; sourceTree = ""; }; 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; - 9BC3BD8CAFAE0C8EB92CD04E5FC24E61 /* Pods-iosApp-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-iosApp-dummy.m"; sourceTree = ""; }; - 9C2AE2A8EE9D646E01A4BDE4ADF28F70 /* shared.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = shared.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; - 9C49AEBC7AA7C80C03295804C6F07963 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-iosApp.release.xcconfig"; sourceTree = ""; }; + 76A263267985B0185D85E24D40FCEE9A /* Pods-iosApp-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iosApp-Info.plist"; sourceTree = ""; }; + 76D28488EA8CF5C697DFF07967A9960E /* Pods-iosApp-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iosApp-acknowledgements.plist"; sourceTree = ""; }; 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; B097DD7534E741D5C41838011D755842 /* Pods-iosApp */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-iosApp"; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B4187E3882740F50F47FCC9A22D9BF99 /* shared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = shared.debug.xcconfig; sourceTree = ""; }; - F6DF6FB4000E345BDEE186C956C36ABF /* Pods-iosApp-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iosApp-acknowledgements.plist"; sourceTree = ""; }; - F981EE0C95E2DFD40CA16F05D2C35B8A /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; - FB978CA3A69A4DEF4DC035E9CD8D83A4 /* Pods-iosApp.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-iosApp.modulemap"; sourceTree = ""; }; + B7C6BC0A7177E1D2CCCA8D9834206E64 /* shared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = shared.release.xcconfig; sourceTree = ""; }; + E462E23B3674BF94EAB1504D506F2803 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; + E4C923318724794E3CC670804C2D6A6B /* Pods-iosApp-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-iosApp-acknowledgements.markdown"; sourceTree = ""; }; + E6DB2A5F5DADA1DDE45F36B1A2D6AC16 /* Pods-iosApp-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-iosApp-dummy.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -67,12 +70,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 11C970DEAE48C6D0282DFE54684F53F1 /* Targets Support Files */ = { + 0BDC589A39A26B9CF449BC4DB7EAC506 /* shared */ = { isa = PBXGroup; children = ( - 4C16E8CC03E90AF9CABF8C82B813AE97 /* Pods-iosApp */, + 15C62982CC8B1FD56214604CE982F646 /* compose-resources */, + DFA1DB9AC4A7CFB0C413CC312D0FE2B7 /* Frameworks */, + 2D40657AD4B86DBF0DA60AE3E13CDCB8 /* Pod */, + DAC212B1D8E90084ABE25A3B3FFC6E00 /* Support Files */, ); - name = "Targets Support Files"; + name = shared; + path = ../../shared; sourceTree = ""; }; 1F86AA6785DF34AFD5A71790761717DE /* Products */ = { @@ -83,31 +90,14 @@ name = Products; sourceTree = ""; }; - 2D583B4AD8D3EF414E0A5ACACD123506 /* Pod */ = { + 2D40657AD4B86DBF0DA60AE3E13CDCB8 /* Pod */ = { isa = PBXGroup; children = ( - 9C2AE2A8EE9D646E01A4BDE4ADF28F70 /* shared.podspec */, + 5F931FED0EB9A618FB9EFF402884E1AC /* shared.podspec */, ); name = Pod; sourceTree = ""; }; - 4C16E8CC03E90AF9CABF8C82B813AE97 /* Pods-iosApp */ = { - isa = PBXGroup; - children = ( - FB978CA3A69A4DEF4DC035E9CD8D83A4 /* Pods-iosApp.modulemap */, - 4D3E6DCB9CAB65A8A05C467E2BBC1F0D /* Pods-iosApp-acknowledgements.markdown */, - F6DF6FB4000E345BDEE186C956C36ABF /* Pods-iosApp-acknowledgements.plist */, - 9BC3BD8CAFAE0C8EB92CD04E5FC24E61 /* Pods-iosApp-dummy.m */, - 6A3C5EB0586A09C512019B6B6A2DE103 /* Pods-iosApp-Info.plist */, - 257390D34074D2442461A69FE6970CBD /* Pods-iosApp-resources.sh */, - 70E8DFC7821955063C886C71258CBE53 /* Pods-iosApp-umbrella.h */, - F981EE0C95E2DFD40CA16F05D2C35B8A /* Pods-iosApp.debug.xcconfig */, - 9C49AEBC7AA7C80C03295804C6F07963 /* Pods-iosApp.release.xcconfig */, - ); - name = "Pods-iosApp"; - path = "Target Support Files/Pods-iosApp"; - sourceTree = ""; - }; 578452D2E740E91742655AC8F1636D1F /* iOS */ = { isa = PBXGroup; children = ( @@ -119,11 +109,29 @@ 58AAD176B64323B9974E5B70EC8B12DC /* Development Pods */ = { isa = PBXGroup; children = ( - E81EDF3E1084CBB7A3F29E8ACBCFFB3C /* shared */, + 0BDC589A39A26B9CF449BC4DB7EAC506 /* shared */, ); name = "Development Pods"; sourceTree = ""; }; + BA6B7BC2729F657E9D3682E55CA6E980 /* Pods-iosApp */ = { + isa = PBXGroup; + children = ( + 11C9B998CE0869936AE6BE69270DAAC9 /* Pods-iosApp.modulemap */, + E4C923318724794E3CC670804C2D6A6B /* Pods-iosApp-acknowledgements.markdown */, + 76D28488EA8CF5C697DFF07967A9960E /* Pods-iosApp-acknowledgements.plist */, + E6DB2A5F5DADA1DDE45F36B1A2D6AC16 /* Pods-iosApp-dummy.m */, + 55ABB06C8A1800962A74E007E7733796 /* Pods-iosApp-frameworks.sh */, + 76A263267985B0185D85E24D40FCEE9A /* Pods-iosApp-Info.plist */, + 6F3B5FED07117E377CFAC3D49B490F17 /* Pods-iosApp-resources.sh */, + 015E0D7EA7331961AB63E5AFECA86BB5 /* Pods-iosApp-umbrella.h */, + E462E23B3674BF94EAB1504D506F2803 /* Pods-iosApp.debug.xcconfig */, + 244FC2DAA936A0B07ACEFDC74E2D54B8 /* Pods-iosApp.release.xcconfig */, + ); + name = "Pods-iosApp"; + path = "Target Support Files/Pods-iosApp"; + sourceTree = ""; + }; CF1408CF629C7361332E53B88F7BD30C = { isa = PBXGroup; children = ( @@ -131,7 +139,7 @@ 58AAD176B64323B9974E5B70EC8B12DC /* Development Pods */, D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */, 1F86AA6785DF34AFD5A71790761717DE /* Products */, - 11C970DEAE48C6D0282DFE54684F53F1 /* Targets Support Files */, + D456857FB6E5BC3266BEC21401D60DB5 /* Targets Support Files */, ); sourceTree = ""; }; @@ -143,34 +151,31 @@ name = Frameworks; sourceTree = ""; }; - DB421E7D1136C3D8E612259E1920A7E9 /* Support Files */ = { + D456857FB6E5BC3266BEC21401D60DB5 /* Targets Support Files */ = { isa = PBXGroup; children = ( - B4187E3882740F50F47FCC9A22D9BF99 /* shared.debug.xcconfig */, - 293144EFA3ECD0A4AF2FCAD935435823 /* shared.release.xcconfig */, + BA6B7BC2729F657E9D3682E55CA6E980 /* Pods-iosApp */, ); - name = "Support Files"; - path = "../ios/Pods/Target Support Files/shared"; + name = "Targets Support Files"; sourceTree = ""; }; - DCF25A5DD47FA7C0A43D3A834F4B5263 /* Frameworks */ = { + DAC212B1D8E90084ABE25A3B3FFC6E00 /* Support Files */ = { isa = PBXGroup; children = ( - 2310A0FF4C69C2DB4993B8D41A756CB4 /* shared.framework */, + 45CF8D032A049A7012F30257A8F121AF /* shared-copy-dsyms.sh */, + 215D65FA831B0E4F8AC0B3826A846FDA /* shared.debug.xcconfig */, + B7C6BC0A7177E1D2CCCA8D9834206E64 /* shared.release.xcconfig */, ); - name = Frameworks; + name = "Support Files"; + path = "../ios/Pods/Target Support Files/shared"; sourceTree = ""; }; - E81EDF3E1084CBB7A3F29E8ACBCFFB3C /* shared */ = { + DFA1DB9AC4A7CFB0C413CC312D0FE2B7 /* Frameworks */ = { isa = PBXGroup; children = ( - 433D35B85989957C756599EEE9AA3B8A /* compose-resources */, - DCF25A5DD47FA7C0A43D3A834F4B5263 /* Frameworks */, - 2D583B4AD8D3EF414E0A5ACACD123506 /* Pod */, - DB421E7D1136C3D8E612259E1920A7E9 /* Support Files */, + 670597F16E31933FDD530ED5140D4EEF /* shared.framework */, ); - name = shared; - path = ../../shared; + name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ @@ -245,7 +250,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - BEA8885189D408D600647BDC228A6A20 /* [CP-User] Build shared */ = { + 11F372A9E68C08375C21A8D47B3CDB02 /* [CP-User] Build shared */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -255,6 +260,23 @@ shellPath = /bin/sh; shellScript = " if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"\"\n exit 0\n fi\n set -ev\n REPO_ROOT=\"$PODS_TARGET_SRCROOT\"\n \"$REPO_ROOT/../gradlew\" -p \"$REPO_ROOT\" $KOTLIN_PROJECT_PATH:syncFramework -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME -Pkotlin.native.cocoapods.archs=\"$ARCHS\" -Pkotlin.native.cocoapods.configuration=\"$CONFIGURATION\"\n"; }; + 35C7D8836447FEC746DA683E14000153 /* [CP] Copy dSYMs */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/shared/shared-copy-dsyms-input-files.xcfilelist", + ); + name = "[CP] Copy dSYMs"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/shared/shared-copy-dsyms-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/shared/shared-copy-dsyms.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -280,7 +302,7 @@ /* Begin XCBuildConfiguration section */ 02DDCCED053337F381DEBAFDEC6F354F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9C49AEBC7AA7C80C03295804C6F07963 /* Pods-iosApp.release.xcconfig */; + baseConfigurationReference = 244FC2DAA936A0B07ACEFDC74E2D54B8 /* Pods-iosApp.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ENABLE_OBJC_WEAK = NO; @@ -378,6 +400,23 @@ }; name = Release; }; + 8E1767EF3E210BC4C8DDA6D5A57FC3C6 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 215D65FA831B0E4F8AC0B3826A846FDA /* shared.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_OBJC_WEAK = NO; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; A0374B8CF9A7D6A45F6D116D698D1C19 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -444,44 +483,9 @@ }; name = Debug; }; - A7B4967B71249851CABD6EC29251E481 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = B4187E3882740F50F47FCC9A22D9BF99 /* shared.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ENABLE_OBJC_WEAK = NO; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - A96D4527F178BD8C0DEB7EE72B9AAE09 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 293144EFA3ECD0A4AF2FCAD935435823 /* shared.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ENABLE_OBJC_WEAK = NO; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; AF088B6CD92A52AC4DCB62DEEC871231 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F981EE0C95E2DFD40CA16F05D2C35B8A /* Pods-iosApp.debug.xcconfig */; + baseConfigurationReference = E462E23B3674BF94EAB1504D506F2803 /* Pods-iosApp.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ENABLE_OBJC_WEAK = NO; @@ -516,6 +520,24 @@ }; name = Debug; }; + E387420B1FEC2EAB6CA01993F647E86F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B7C6BC0A7177E1D2CCCA8D9834206E64 /* shared.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_OBJC_WEAK = NO; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -528,20 +550,20 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 8349D8E2EC974421A14EF8ABFF6AD6DC /* Build configuration list for PBXAggregateTarget "shared" */ = { + 9F1E85ECB672A0CC96333A6C6DF60EE6 /* Build configuration list for PBXNativeTarget "Pods-iosApp" */ = { isa = XCConfigurationList; buildConfigurations = ( - A7B4967B71249851CABD6EC29251E481 /* Debug */, - A96D4527F178BD8C0DEB7EE72B9AAE09 /* Release */, + AF088B6CD92A52AC4DCB62DEEC871231 /* Debug */, + 02DDCCED053337F381DEBAFDEC6F354F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 9F1E85ECB672A0CC96333A6C6DF60EE6 /* Build configuration list for PBXNativeTarget "Pods-iosApp" */ = { + B999BD58A146737F72CA68509B0BCE77 /* Build configuration list for PBXAggregateTarget "shared" */ = { isa = XCConfigurationList; buildConfigurations = ( - AF088B6CD92A52AC4DCB62DEEC871231 /* Debug */, - 02DDCCED053337F381DEBAFDEC6F354F /* Release */, + 8E1767EF3E210BC4C8DDA6D5A57FC3C6 /* Debug */, + E387420B1FEC2EAB6CA01993F647E86F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/ios/Pods/Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig b/ios/Pods/Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig index 60daa9a..3f303bf 100644 --- a/ios/Pods/Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig +++ b/ios/Pods/Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig @@ -2,7 +2,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/../../shared/build/cocoapods/framework" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' -OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -framework "shared" +OTHER_LDFLAGS = $(inherited) -l"c++" -framework "shared" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_PODFILE_DIR_PATH = ${SRCROOT}/. diff --git a/ios/Pods/Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig b/ios/Pods/Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig index 60daa9a..3f303bf 100644 --- a/ios/Pods/Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig +++ b/ios/Pods/Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig @@ -2,7 +2,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/../../shared/build/cocoapods/framework" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' -OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -framework "shared" +OTHER_LDFLAGS = $(inherited) -l"c++" -framework "shared" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_PODFILE_DIR_PATH = ${SRCROOT}/. diff --git a/ios/iosApp.xcodeproj/project.pbxproj b/ios/iosApp.xcodeproj/project.pbxproj index e088e53..123bfc1 100644 --- a/ios/iosApp.xcodeproj/project.pbxproj +++ b/ios/iosApp.xcodeproj/project.pbxproj @@ -131,6 +131,7 @@ 7555FF79242A565900829871 /* Resources */, 7555FFB4242A642300829871 /* Embed Frameworks */, 21256BB7B14DAA1E36104A70 /* [CP] Copy Pods Resources */, + 270999094322AED28443355A /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -204,6 +205,23 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 270999094322AED28443355A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 7555FFB5242A651A00829871 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/ios/iosApp/di/DiModule.swift b/ios/iosApp/di/DiModule.swift index 97915d5..43086ab 100644 --- a/ios/iosApp/di/DiModule.swift +++ b/ios/iosApp/di/DiModule.swift @@ -10,5 +10,11 @@ import Foundation import shared class DiModule { - static var koin = { InitKoin().invoke() }() + static var koin = { + KoinInit().doInit( + appDeclaration: { _ in + // Do nothing + } + ) + }() } diff --git a/ios/iosApp/iOSApp.swift b/ios/iosApp/iOSApp.swift index c996c58..7e5f2ad 100644 --- a/ios/iosApp/iOSApp.swift +++ b/ios/iosApp/iOSApp.swift @@ -1,8 +1,12 @@ import SwiftUI import shared +import UIKit @main struct iOSApp: App { + init() { + DiModule.koin + } var body: some Scene { WindowGroup { ComposeView() @@ -15,8 +19,6 @@ struct ComposeView: UIViewControllerRepresentable { // File name "Main" + "Kt" -> "Function Name" return MainKt.MainViewController() } - - func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { - - } -} + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} +} \ No newline at end of file diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..8f1c4e6 --- /dev/null +++ b/renovate.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ], + "schedule": [ + "on friday" + ], + "timezone": "Africa/Nairobi", + "labels": [ + "dependency-update" + ], + "prHourlyLimit": 0, + "baseBranches": [ + "develop" + ], + "separateMultipleMajor": true, + "dependencyDashboardTitle": "automated dependency updates dashboard", + "dependencyDashboard": true, + "branchPrefix": "chore/", + "additionalBranchPrefix": "update-libs/", + "commitMessageAction": "update", + "commitMessageExtra": "from {{{currentValue}}} to {{#if isPinDigest}}{{{newDigestShort}}}{{else}}{{#if isMajor}}{{prettyNewMajor}}{{else}}{{#if isSingleVersion}}{{prettyNewVersion}}{{else}}{{#if newValue}}{{{newValue}}}{{else}}{{{newDigestShort}}}{{/if}}{{/if}}{{/if}}{{/if}}", + "packageRules": [ + { + "groupName": "all non-major dependencies", + "groupSlug": "all-minor-patch", + "matchPackagePatterns": [ + "*" + ], + "matchUpdateTypes": [ + "minor", + "patch" + ] + }, + { + "groupName": "kotlin dependencies", + "matchPackagePatterns": [ + "org.jetbrains.kotlin:*", + "com.google.devtools.ksp", + "composeOptions" + ] + }, + { + "groupName": "coroutine dependencies", + "matchPackagePatterns": [ + "io.coil-kt:*", + "org.jetbrains.kotlinx:*" + ] + }, + { + "groupName": "plugin dependencies", + "matchPackagePatterns": [ + "com.android.library", + "com.android.application", + "app.cash.paparazzi" + ] + }, + { + "groupName": "sonar", + "matchPackagePatterns": [ + "org.sonarqube" + ] + }, + { + "groupName": "target sdk 34", + "matchPackagePatterns": [ + "androidx.navigation:navigation-compose" + ] + }, + { + "groupName": "ktlint", + "matchPackagePatterns": [ + "org.jlleitschuh.gradle.ktlint" + ] + }, + { + "groupName": "test dependencies", + "matchPackagePatterns": [ + "com.google.truth:truth", + "androidx.compose.ui:ui-test-junit4", + "androidx.compose.ui:ui-test-manifest", + "org.robolectric:robolectric", + "junit:junit", + "androidx.test:core-ktx" + ] + } + ], + "ignoreDeps": [ + "androidx.emoji2:emoji2" + ] +} \ No newline at end of file diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 455ff2b..2c757e6 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + plugins { alias(libs.plugins.multiplatform) alias(libs.plugins.android.library) @@ -41,7 +57,7 @@ kotlin { podfile = project.file("../ios/Podfile") framework { baseName = "shared" - isStatic = true + isStatic = false } } @@ -58,6 +74,7 @@ kotlin { val commonMain by getting { dependencies { api(libs.koin.core) + api(libs.koin.compose) implementation(compose.material3) implementation(compose.material) @@ -77,31 +94,31 @@ kotlin { implementation(libs.sqlDelight.runtime) implementation(libs.sqlDelight.coroutine) + implementation(libs.primitive.adapters) - implementation(libs.multiplatformSettings.noArg) - implementation(libs.multiplatformSettings.coroutines) + api(libs.multiplatformSettings.noArg) + api(libs.multiplatformSettings.coroutines) api(libs.napier) implementation(libs.kotlinX.dateTime) - } - } + implementation(libs.koalaplot.core) - val commonTest by getting { - dependencies { - dependsOn(commonMain) + implementation(libs.korau) } } val androidMain by getting { dependencies { implementation(libs.sqlDelight.android) + implementation(libs.accompanist.systemUIController) } } val jvmMain by getting { dependencies { implementation(libs.sqlDelight.jvm) + implementation(libs.kotlinx.coroutines.swing) } } @@ -122,8 +139,8 @@ kotlin { } sqldelight { - database(name = "AppDatabase") { - packageName = "com.joelkanyi.focusbloom.data.cache.sqldelight" - sourceFolders = listOf("kotlin") + database("BloomDatabase") { + packageName = "com.joelkanyi.focusbloom.database" + sourceFolders = listOf("sqldelight") } } diff --git a/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/di/Module.kt b/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/di/Module.kt new file mode 100644 index 0000000..c542825 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/di/Module.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.di + +import com.joelkanyi.focusbloom.platform.DatabaseDriverFactory +import com.joelkanyi.focusbloom.platform.MultiplatformSettingsWrapper +import com.russhwolf.settings.ExperimentalSettingsApi +import org.koin.core.module.Module +import org.koin.dsl.module + +@OptIn(ExperimentalSettingsApi::class) +actual fun platformModule(): Module = module { + single { MultiplatformSettingsWrapper(context = get()).createSettings() } + single { DatabaseDriverFactory(context = get()) } +} diff --git a/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.android.kt b/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.android.kt new file mode 100644 index 0000000..ec0f599 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.android.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import android.content.Context +import com.joelkanyi.focusbloom.database.BloomDatabase +import com.squareup.sqldelight.android.AndroidSqliteDriver +import com.squareup.sqldelight.db.SqlDriver + +actual class DatabaseDriverFactory(private val context: Context) { + actual fun createDriver(): SqlDriver { + return AndroidSqliteDriver(BloomDatabase.Schema, context, "bloom.db") + } +} diff --git a/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/Font.android.kt b/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/Font.android.kt index 8450f81..6585ad5 100644 --- a/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/Font.android.kt +++ b/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/Font.android.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.platform import androidx.compose.runtime.Composable diff --git a/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.android.kt b/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.android.kt new file mode 100644 index 0000000..7ad3738 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.android.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import android.content.Context +import com.russhwolf.settings.AndroidSettings +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.ObservableSettings + +actual class MultiplatformSettingsWrapper(private val context: Context) { + @OptIn(ExperimentalSettingsApi::class) + actual fun createSettings(): ObservableSettings { + val sharedPreferences = + context.getSharedPreferences("bloom_preferences", Context.MODE_PRIVATE) + return AndroidSettings(delegate = sharedPreferences) + } +} diff --git a/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.android.kt b/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.android.kt new file mode 100644 index 0000000..d90f9c2 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.android.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import com.google.accompanist.systemuicontroller.rememberSystemUiController + +@Composable +actual fun StatusBarColors(statusBarColor: Color, navBarColor: Color) { + val sysUiController = rememberSystemUiController() + SideEffect { + sysUiController.setSystemBarsColor(color = statusBarColor) + sysUiController.setNavigationBarColor(color = navBarColor) + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/FocusBloomApp.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/FocusBloomApp.kt index 7815c6a..bf8a8b4 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/FocusBloomApp.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/FocusBloomApp.kt @@ -1,160 +1,76 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.BottomNavigation -import androidx.compose.material.BottomNavigationItem -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.FabPosition -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.FloatingActionButtonDefaults -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi -import cafe.adriel.voyager.navigator.CurrentScreen -import cafe.adriel.voyager.navigator.tab.CurrentTab -import cafe.adriel.voyager.navigator.tab.LocalTabNavigator -import cafe.adriel.voyager.navigator.tab.Tab -import cafe.adriel.voyager.navigator.tab.TabDisposable -import cafe.adriel.voyager.navigator.tab.TabNavigator -import com.joelkanyi.focusbloom.core.presentation.component.BloomNavigationRailBar -import com.joelkanyi.focusbloom.core.presentation.component.BloomTab +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.transitions.SlideTransition import com.joelkanyi.focusbloom.core.presentation.theme.FocusBloomTheme -import com.joelkanyi.focusbloom.core.presentation.utils.FilledIcon +import com.joelkanyi.focusbloom.core.utils.ProvideAppNavigator +import com.joelkanyi.focusbloom.feature.onboarding.OnboardingScreen +import com.joelkanyi.focusbloom.main.MainScreen +import com.joelkanyi.focusbloom.main.MainViewModel +import com.joelkanyi.focusbloom.main.OnBoardingState +import com.joelkanyi.focusbloom.platform.StatusBarColors +import org.koin.compose.rememberKoinInject -@OptIn( - ExperimentalVoyagerApi::class, - ExperimentalMaterial3WindowSizeClassApi::class, -) @Composable fun FocusBloomApp() { - FocusBloomTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, - ) { - val windowSizeClass = calculateWindowSizeClass() - val useNavRail = windowSizeClass.widthSizeClass > WindowWidthSizeClass.Compact + val mainViewModel = rememberKoinInject() + val darkTheme = when (mainViewModel.appTheme.collectAsState().value) { + 1 -> true + else -> false + } + val onBoardingCompleted = mainViewModel.onBoardingCompleted.collectAsState().value - TabNavigator( - BloomTab.HomeTab, - tabDisposable = { - TabDisposable( - navigator = it, - tabs = listOf( - BloomTab.HomeTab, - BloomTab.StatisticsTab, - BloomTab.SettingsTab, - BloomTab.CalendarTab, - ), - ) - }, - ) { - val tabNavigator = LocalTabNavigator.current - val showNavRailOrBottomBar = true - if (useNavRail) { - Row { - if (showNavRailOrBottomBar) { - BloomNavigationRailBar( - tabNavigator = it, - navRailItems = listOf( - BloomTab.HomeTab, - BloomTab.CalendarTab, - BloomTab.AddTaskTab, - BloomTab.StatisticsTab, - BloomTab.SettingsTab, - ), + FocusBloomTheme( + useDarkTheme = darkTheme + ) { + StatusBarColors( + statusBarColor = MaterialTheme.colorScheme.background, + navBarColor = MaterialTheme.colorScheme.background + ) + when (onBoardingCompleted) { + is OnBoardingState.Success -> { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Navigator( + screen = if (onBoardingCompleted.completed) { + MainScreen() + } else { + OnboardingScreen() + }, + content = { navigator -> + ProvideAppNavigator( + navigator = navigator, + content = { SlideTransition(navigator = navigator) } ) } - - CurrentScreen() - } - } else { - Scaffold( - content = { - CurrentTab() - }, - floatingActionButtonPosition = FabPosition.Center, - floatingActionButton = { - FloatingActionButton( - modifier = Modifier - .offset(y = 60.dp) - .size(42.dp), - containerColor = MaterialTheme.colorScheme.primary, - onClick = { - tabNavigator.current = BloomTab.AddTaskTab - }, - elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 0.dp), - shape = CircleShape, - ) { - Icon( - imageVector = Icons.Filled.Add, - contentDescription = "", - tint = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.size(24.dp), - ) - } - }, - bottomBar = { - if (showNavRailOrBottomBar) { - BottomNavigation( - backgroundColor = MaterialTheme.colorScheme.background, - ) { - TabNavigationItem(BloomTab.HomeTab) - TabNavigationItem(BloomTab.CalendarTab) - TabNavigationItem(BloomTab.StatisticsTab) - TabNavigationItem(BloomTab.SettingsTab) - } - } - }, ) } } + + else -> {} } } } - -@Composable -private fun RowScope.TabNavigationItem(tab: Tab) { - val tabNavigator = LocalTabNavigator.current - val isSelected = tabNavigator.current == tab - - BottomNavigationItem( - modifier = Modifier.offset( - x = when (tab.options.index) { - (0u).toUShort() -> 0.dp - (1u).toUShort() -> (-24).dp - (2u).toUShort() -> 24.dp - (3u).toUShort() -> 0.dp - else -> 0.dp - }, - ), - selected = tabNavigator.current == tab, - onClick = { tabNavigator.current = tab }, - icon = { - tab.options.icon?.let { - Icon( - painter = if (isSelected) { - FilledIcon(tab) - } else { - it - }, - contentDescription = tab.options.title, - ) - } - }, - ) -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/calendar/CalendarScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/calendar/CalendarScreen.kt deleted file mode 100644 index b847648..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/calendar/CalendarScreen.kt +++ /dev/null @@ -1,460 +0,0 @@ -@file:OptIn(ExperimentalFoundationApi::class) - -package com.joelkanyi.focusbloom.calendar - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.pager.PagerState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.times -import cafe.adriel.voyager.core.screen.Screen -import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar -import com.joelkanyi.focusbloom.core.samples.sampleTasks -import com.joelkanyi.focusbloom.core.utils.MAX -import com.joelkanyi.focusbloom.core.utils.MIN -import com.joelkanyi.focusbloom.core.utils.PositionedTask -import com.joelkanyi.focusbloom.core.utils.ScheduleSize -import com.joelkanyi.focusbloom.core.utils.SplitType -import com.joelkanyi.focusbloom.core.utils.arrangeTasks -import com.joelkanyi.focusbloom.core.utils.differenceBetweenDays -import com.joelkanyi.focusbloom.core.utils.differenceBetweenMinutes -import com.joelkanyi.focusbloom.core.utils.dpToPx -import com.joelkanyi.focusbloom.core.utils.plusHours -import com.joelkanyi.focusbloom.core.utils.splitTasks -import com.joelkanyi.focusbloom.core.utils.taskData -import com.joelkanyi.focusbloom.core.utils.truncatedTo -import com.joelkanyi.focusbloom.domain.model.Task -import kotlinx.coroutines.launch -import kotlinx.datetime.Clock -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalTime -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import kotlin.math.roundToInt - -class CalendarScreen : Screen { - @Composable - override fun Content() { - CalendarScreenContent() - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CalendarScreenContent() { - Scaffold( - topBar = { - BloomTopAppBar( - hasBackNavigation = false, - ) { - Text(text = "Calendar") - } - }, - ) { paddingValues -> - var selectedDay by remember { - mutableStateOf(Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date) - } - Column( - modifier = Modifier - .padding(paddingValues) - .padding(PaddingValues(horizontal = 16.dp)), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - /*HorizontalCalendarView( - modifier = Modifier.fillMaxWidth(), - selectedTextColor = MaterialTheme.colorScheme.onPrimary, - unSelectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, - selectedCardColor = MaterialTheme.colorScheme.primary, - unSelectedCardColor = MaterialTheme.colorScheme.surfaceVariant, - onDayClick = { day -> - selectedDay = day.fullDate.localDate() - // Toast.makeText(context, day.toString(), Toast.LENGTH_SHORT).show() - }, - )*/ - - val todaysTasks = - sampleTasks.filter { it.start.date.dayOfMonth == selectedDay.dayOfMonth } - if (todaysTasks.isNotEmpty()) { - Schedule( - tasks = todaysTasks.sortedBy { it.start }, - ) - } else { - Box(modifier = Modifier.fillMaxSize()) { - Text( - text = "No tasks for today", - style = MaterialTheme.typography.labelLarge, - textAlign = TextAlign.Center, - modifier = Modifier.align(Alignment.Center), - ) - } - } - } - } -} - -@Composable -fun BasicTask( - positionedTask: PositionedTask, - modifier: Modifier = Modifier, -) { - val task = positionedTask.task - val topRadius = - if (positionedTask.splitType == SplitType.Start || positionedTask.splitType == SplitType.Both) 0.dp else 4.dp - val bottomRadius = - if (positionedTask.splitType == SplitType.End || positionedTask.splitType == SplitType.Both) 0.dp else 4.dp - Column( - modifier = modifier - .fillMaxSize() - .padding( - top = 2.dp, - end = 2.dp, - bottom = if (positionedTask.splitType == SplitType.End) 0.dp else 2.dp, - ) - .clipToBounds() - .background( - color = Color(task.color), - shape = RoundedCornerShape( - topStart = topRadius, - topEnd = topRadius, - bottomEnd = bottomRadius, - bottomStart = bottomRadius, - ), - ) - .padding(4.dp), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = "${task.start.time} - ${task.end.time}", - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Clip, - ) - - Text( - text = task.name, - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - if (task.description != null) { - Text( - text = task.description!!, - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } -} - -@Composable -fun BasicSidebarLabel( - time: LocalTime, - modifier: Modifier = Modifier, -) { - Text( - text = "${time.hour}:00", - modifier = modifier - .fillMaxHeight() - .padding(4.dp), - ) -} - -@Composable -fun ScheduleSidebar( - hourHeight: Dp, - modifier: Modifier = Modifier, - minTime: LocalTime = Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).time.MIN(), - maxTime: LocalTime = Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).time.MAX(), - label: @Composable (time: LocalTime) -> Unit = { BasicSidebarLabel(time = it) }, -) { - val numMinutes = differenceBetweenMinutes(minTime, maxTime).toInt() + 1 - val numHours = numMinutes / 60 - val firstHour = minTime.truncatedTo() - val firstHourOffsetMinutes = - if (firstHour == minTime) 0 else differenceBetweenMinutes(minTime, firstHour.plusHours(1)) - val firstHourOffset = hourHeight * (firstHourOffsetMinutes / 60f) - val startTime = if (firstHour == minTime) firstHour else firstHour.plusHours(1) - Column(modifier = modifier) { - Spacer(modifier = Modifier.height(firstHourOffset)) - repeat(numHours) { i -> - Box(modifier = Modifier.height(hourHeight)) { - label(startTime.plusHours(i)) - } - } - } -} - -@Composable -fun Schedule( - tasks: List, - modifier: Modifier = Modifier, - taskContent: @Composable (positionedTask: PositionedTask) -> Unit = { - BasicTask( - positionedTask = it, - ) - }, - timeLabel: @Composable (time: LocalTime) -> Unit = { BasicSidebarLabel(time = it) }, - minDate: LocalDate = tasks.minByOrNull(Task::start)?.start?.date ?: Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).date, - maxDate: LocalDate = tasks.maxByOrNull(Task::end)?.end?.date ?: Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).date, - minTime: LocalTime = Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).time.MIN(), - maxTime: LocalTime = Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).time.MAX(), - daySize: ScheduleSize = ScheduleSize.FixedSize(300.dp), - hourSize: ScheduleSize = ScheduleSize.FixedSize(64.dp), -) { - val numDays = 0 + 1 - val numMinutes = differenceBetweenMinutes(minTime, maxTime).toInt() + 1 - val numHours = numMinutes.toFloat() / 60f - val verticalScrollState = rememberScrollState() - val scope = rememberCoroutineScope() - - /** - * Scroll to the closest task - * */ - // LaunchedEffect(key1 = tasks, block = { - when (hourSize) { - is ScheduleSize.Adaptive -> { - val task = tasks.minByOrNull { it.start } - if (task != null) { - val taskStartMinutes = differenceBetweenMinutes(minTime, task.start.time) - val taskStartHours = taskStartMinutes / 60f - val taskStartOffset = taskStartHours * hourSize.minSize.dpToPx() - LaunchedEffect(key1 = tasks, block = { - scope.launch { - verticalScrollState.animateScrollTo(taskStartOffset.roundToInt()) - } - }) - } - } - - is ScheduleSize.FixedCount -> { - val task = tasks.minByOrNull { it.start } - if (task != null) { - val taskStartMinutes = differenceBetweenMinutes(minTime, task.start.time) - val taskStartHours = taskStartMinutes / 60f - val taskStartOffset = taskStartHours * hourSize.count.dp - LaunchedEffect(key1 = tasks, block = { - scope.launch { - verticalScrollState.animateScrollTo(taskStartOffset.value.roundToInt()) - } - }) - } - } - - is ScheduleSize.FixedSize -> { - // Scroll to the closest task - val task = tasks.minByOrNull { it.start } - if (task != null) { - val taskStartMinutes = differenceBetweenMinutes(minTime, task.start.time) - val taskStartHours = taskStartMinutes / 60f - val taskStartOffset = taskStartHours * hourSize.size.dpToPx() - LaunchedEffect(key1 = tasks, block = { - scope.launch { - verticalScrollState.animateScrollTo(taskStartOffset.roundToInt()) - } - }) - } - } - } - // }) - - val horizontalScrollState = rememberScrollState() - var sidebarWidth by remember { mutableStateOf(0) } - // var headerHeight by remember { mutableStateOf(0) } - BoxWithConstraints(modifier = modifier) { - val dayWidth: Dp = when (daySize) { - is ScheduleSize.FixedSize -> daySize.size - is ScheduleSize.FixedCount -> with(LocalDensity.current) { ((constraints.maxWidth - sidebarWidth) / daySize.count).toDp() } - is ScheduleSize.Adaptive -> with(LocalDensity.current) { - maxOf( - ((constraints.maxWidth - sidebarWidth) / numDays).toDp(), - daySize.minSize, - ) - } - } - val hourHeight: Dp = when (hourSize) { - is ScheduleSize.FixedSize -> hourSize.size - is ScheduleSize.FixedCount -> with(LocalDensity.current) { ((constraints.maxHeight) / hourSize.count).toDp() } - is ScheduleSize.Adaptive -> with(LocalDensity.current) { - maxOf( - ((constraints.maxHeight) / numHours).toDp(), - hourSize.minSize, - ) - } - } - Column(modifier = modifier) { - Row( - modifier = Modifier - .weight(1f) - .align(Alignment.Start), - ) { - ScheduleSidebar( - hourHeight = hourHeight, - minTime = minTime, - maxTime = maxTime, - label = timeLabel, - modifier = Modifier - .verticalScroll(verticalScrollState) - .onGloballyPositioned { sidebarWidth = it.size.width }, - ) - BasicSchedule( - tasks = tasks, - taskContent = taskContent, - minDate = minDate, - maxDate = maxDate, - minTime = minTime, - maxTime = maxTime, - dayWidth = dayWidth, - hourHeight = hourHeight, - modifier = Modifier - .weight(1f) - .verticalScroll(verticalScrollState) - .horizontalScroll(horizontalScrollState), - ) - } - } - } -} - -@Composable -fun BasicSchedule( - tasks: List, - modifier: Modifier = Modifier, - taskContent: @Composable (positionedTask: PositionedTask) -> Unit = { - BasicTask( - positionedTask = it, - ) - }, - minDate: LocalDate = tasks.minByOrNull(Task::start)?.start?.date ?: Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).date, - maxDate: LocalDate = tasks.maxByOrNull(Task::end)?.end?.date ?: Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).date, - minTime: LocalTime = Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).time.MIN(), - maxTime: LocalTime = Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).time.MAX(), - dayWidth: Dp, - hourHeight: Dp, -) { - val numDays = differenceBetweenDays(minDate, maxDate).toInt() + 1 - val numMinutes = differenceBetweenMinutes(minTime, maxTime).toInt() + 1 - val numHours = numMinutes / 60 - val dividerColor = - if (androidx.compose.material.MaterialTheme.colors.isLight) Color.LightGray else Color.DarkGray - val positionedTasks = - remember(tasks) { arrangeTasks(splitTasks(tasks.sortedBy(Task::start))).filter { it.end > minTime && it.start < maxTime } } - Layout( - content = { - positionedTasks.forEach { positionedTask -> - Box(modifier = Modifier.taskData(positionedTask)) { - taskContent(positionedTask) - } - } - }, - modifier = modifier - .offset(y = (16).dp) - .drawBehind { - repeat(23) { - drawLine( - dividerColor, - start = Offset(0f, (it + 1) * hourHeight.toPx()), - end = Offset(size.width, (it + 1) * hourHeight.toPx()), - strokeWidth = 1.dp.toPx(), - ) - } - }, - ) { measureables, constraints -> - val height = (hourHeight.toPx() * (numMinutes / 60f)).roundToInt() - val width = dayWidth.roundToPx() * numDays - val placeablesWithTasks = measureables.map { measurable -> - val splitTask = measurable.parentData as PositionedTask - val taskDurationMinutes = - differenceBetweenMinutes(splitTask.start, minOf(splitTask.end, maxTime)) - val taskHeight = ((taskDurationMinutes / 60f) * hourHeight.toPx()).roundToInt() - val taskWidth = - ((splitTask.colSpan.toFloat() / splitTask.colTotal.toFloat()) * dayWidth.toPx()).roundToInt() - val placeable = measurable.measure( - constraints.copy( - minWidth = taskWidth, - maxWidth = taskWidth, - minHeight = taskHeight, - maxHeight = taskHeight, - ), - ) - Pair(placeable, splitTask) - } - layout(width, height) { - placeablesWithTasks.forEach { (placeable, splitTask) -> - val taskOffsetMinutes = if (splitTask.start > minTime) { - differenceBetweenMinutes( - minTime, - splitTask.start, - ) - } else { - 0 - } - val taskY = ((taskOffsetMinutes / 60f) * hourHeight.toPx()).roundToInt() - val taskX = - (splitTask.col * (dayWidth.toPx() / splitTask.colTotal.toFloat())).roundToInt() - placeable.place(taskX, taskY) - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ScheduleWeek( - tasks: List, - setTitle: (String) -> Unit, - pagerState: PagerState, -) { -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/cache/sqldelight/AppDatabase.sq b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/cache/sqldelight/AppDatabase.sq deleted file mode 100644 index e69de29..0000000 diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/local/adapter/DateTimeAdapter.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/local/adapter/DateTimeAdapter.kt new file mode 100644 index 0000000..4078d95 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/local/adapter/DateTimeAdapter.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.data.local.adapter + +import app.cash.sqldelight.ColumnAdapter +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toLocalDateTime + +class DateTimeAdapter : ColumnAdapter { + override fun decode(databaseValue: String): LocalDateTime = databaseValue.toLocalDateTime() + override fun encode(value: LocalDateTime): String = value.toString() +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/local/setting/PreferenceManager.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/local/setting/PreferenceManager.kt new file mode 100644 index 0000000..5cea141 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/local/setting/PreferenceManager.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.data.local.setting + +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.ObservableSettings +import com.russhwolf.settings.coroutines.getBooleanFlow +import com.russhwolf.settings.coroutines.getIntFlow +import com.russhwolf.settings.coroutines.getIntOrNullFlow +import com.russhwolf.settings.coroutines.getLongFlow +import com.russhwolf.settings.coroutines.getStringOrNullFlow +import com.russhwolf.settings.set +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow + +class PreferenceManager constructor(private val observableSettings: ObservableSettings) { + + @OptIn(ExperimentalSettingsApi::class) + fun setString(key: String, value: String) { + observableSettings.set(key = key, value = value) + } + + @OptIn(ExperimentalSettingsApi::class) + fun getNonFlowString(key: String) = observableSettings.getString( + key = key, + defaultValue = "" + ) + + @OptIn(ExperimentalCoroutinesApi::class, ExperimentalSettingsApi::class) + fun getString(key: String) = observableSettings.getStringOrNullFlow(key = key) + + @OptIn(ExperimentalSettingsApi::class) + fun setInt(key: String, value: Int) { + observableSettings.set(key = key, value = value) + } + + @OptIn(ExperimentalSettingsApi::class, ExperimentalCoroutinesApi::class) + fun getInt(key: String) = observableSettings.getIntOrNullFlow(key = key) + + @OptIn(ExperimentalSettingsApi::class, ExperimentalCoroutinesApi::class) + fun getIntFlow(key: String) = observableSettings.getIntFlow(key = key) + + companion object { + const val USERNAME = "username_key" + const val SHORT_BREAK_COLOR = "short_break_color_key" + const val LONG_BREAK_COLOR = "long_break_color_key" + const val FOCUS_COLOR = "focus_color_key" + const val APP_THEME = "app_theme_key" + const val FOCUS_TIME = "focus_time_key" + const val SHORT_BREAK_TIME = "short_break_time_key" + const val LONG_BREAK_TIME = "long_break_time_key" + const val HOUR_FORMAT = "hour_format_key" + } + + @OptIn(ExperimentalSettingsApi::class) + fun clearPreferences() { + observableSettings.clear() + } + + @OptIn(ExperimentalSettingsApi::class, ExperimentalCoroutinesApi::class) + fun getBoolean(key: String): Flow { + return observableSettings.getBooleanFlow( + key = key, + defaultValue = false + ) + } + + @OptIn(ExperimentalSettingsApi::class) + fun setBoolean(key: String, value: Boolean) { + observableSettings.set(key = key, value = value) + } + + @OptIn(ExperimentalCoroutinesApi::class, ExperimentalSettingsApi::class) + fun getLong(key: Any): Flow { + return observableSettings.getLongFlow( + key = key.toString() + ) + } + + @OptIn(ExperimentalSettingsApi::class) + fun setLong(key: String, value: Long) { + observableSettings.set(key = key, value = value) + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/mapper/Mappers.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/mapper/Mappers.kt new file mode 100644 index 0000000..b5a0e9b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/mapper/Mappers.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.data.mapper + +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.utils.dateTimeToString +import database.TaskEntity +import kotlinx.datetime.toLocalDateTime + +fun TaskEntity.toTask() = Task( + id = id, + name = name, + description = description, + type = type, + start = start.toLocalDateTime(), + color = color, + current = current, + date = date.toLocalDateTime(), + focusSessions = focusSessions, + completed = completed, + consumedFocusTime = consumedFocusTime, + consumedShortBreakTime = consumedShortBreakTime, + consumedLongBreakTime = consumedLongBreakTime, + inProgressTask = inProgressTask, + currentCycle = currentCycle, + active = active +) + +fun Task.toTaskEntity() = TaskEntity( + id = id, + name = name, + description = description, + type = type, + start = start.dateTimeToString(), + color = color, + current = current, + date = date.dateTimeToString(), + focusSessions = focusSessions, + completed = completed, + consumedFocusTime = consumedFocusTime, + consumedShortBreakTime = consumedShortBreakTime, + consumedLongBreakTime = consumedLongBreakTime, + inProgressTask = inProgressTask, + currentCycle = currentCycle, + active = active +) diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/repository/settings/SettingsRepositoryImpl.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/repository/settings/SettingsRepositoryImpl.kt new file mode 100644 index 0000000..bd8335f --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/repository/settings/SettingsRepositoryImpl.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.data.repository.settings + +import com.joelkanyi.focusbloom.core.data.local.setting.PreferenceManager +import com.joelkanyi.focusbloom.core.domain.repository.settings.SettingsRepository +import kotlinx.coroutines.flow.Flow + +class SettingsRepositoryImpl( + private val preferenceManager: PreferenceManager +) : SettingsRepository { + override suspend fun saveAppTheme(theme: Int) { + preferenceManager.setInt(key = PreferenceManager.APP_THEME, value = theme) + } + + override fun getAppTheme(): Flow { + return preferenceManager.getInt(key = PreferenceManager.APP_THEME) + } + + override fun clearAll() { + return preferenceManager.clearPreferences() + } + + override fun getSessionTime(): Flow { + return preferenceManager.getInt(key = PreferenceManager.FOCUS_TIME) + } + + override fun getShortBreakTime(): Flow { + return preferenceManager.getInt(key = PreferenceManager.SHORT_BREAK_TIME) + } + + override fun getLongBreakTime(): Flow { + return preferenceManager.getInt(key = PreferenceManager.LONG_BREAK_TIME) + } + + override fun getHourFormat(): Flow { + return preferenceManager.getInt(key = PreferenceManager.HOUR_FORMAT) + } + + override fun saveSessionTime(sessionTime: Int) { + preferenceManager.setInt(key = PreferenceManager.FOCUS_TIME, value = sessionTime) + } + + override fun saveLongBreakTime(longBreakTime: Int) { + preferenceManager.setInt(key = PreferenceManager.LONG_BREAK_TIME, value = longBreakTime) + } + + override fun saveHourFormat(timeFormat: Int) { + preferenceManager.setInt(key = PreferenceManager.HOUR_FORMAT, value = timeFormat) + } + + override fun saveShortBreakTime(shortBreakTime: Int) { + preferenceManager.setInt(key = PreferenceManager.SHORT_BREAK_TIME, value = shortBreakTime) + } + + override fun shortBreakColor(): Flow { + return preferenceManager.getLong(key = PreferenceManager.SHORT_BREAK_COLOR) + } + + override fun saveShortBreakColor(color: Long) { + preferenceManager.setLong(key = PreferenceManager.SHORT_BREAK_COLOR, value = color) + } + + override fun longBreakColor(): Flow { + return preferenceManager.getLong(key = PreferenceManager.LONG_BREAK_COLOR) + } + + override fun saveLongBreakColor(color: Long) { + preferenceManager.setLong(key = PreferenceManager.LONG_BREAK_COLOR, value = color) + } + + override fun focusColor(): Flow { + return preferenceManager.getLong(key = PreferenceManager.FOCUS_COLOR) + } + + override fun saveFocusColor(color: Long) { + preferenceManager.setLong(key = PreferenceManager.FOCUS_COLOR, value = color) + } + + override fun saveUsername(value: String) { + preferenceManager.setString(key = PreferenceManager.USERNAME, value = value) + } + + override fun getUsername(): Flow { + return preferenceManager.getString(key = PreferenceManager.USERNAME) + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/repository/tasks/TasksRepositoryImpl.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/repository/tasks/TasksRepositoryImpl.kt new file mode 100644 index 0000000..9848faf --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/data/repository/tasks/TasksRepositoryImpl.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.data.repository.tasks + +import com.joelkanyi.focusbloom.core.data.mapper.toTask +import com.joelkanyi.focusbloom.core.data.mapper.toTaskEntity +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.domain.repository.tasks.TasksRepository +import com.joelkanyi.focusbloom.database.BloomDatabase +import com.squareup.sqldelight.runtime.coroutines.asFlow +import com.squareup.sqldelight.runtime.coroutines.mapToList +import com.squareup.sqldelight.runtime.coroutines.mapToOneNotNull +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class TasksRepositoryImpl( + bloomDatabase: BloomDatabase +) : TasksRepository { + private val dbQuery = bloomDatabase.taskQueries + override fun getTasks(): Flow> { + return dbQuery + .getAllTasks() + .asFlow() + .mapToList() + .map { tasks -> + tasks.map { + it.toTask() + } + } + } + + override fun getTask(id: Int): Flow { + return dbQuery + .getTaskById(id) + .asFlow() + .mapToOneNotNull() + .map { taskEntity -> + taskEntity.toTask() + } + } + + override suspend fun addTask(task: Task) { + task.toTaskEntity().let { + dbQuery.insertTask( + name = it.name, + description = it.description, + start = it.start, + // end = it.end, + color = it.color, + current = it.current, + date = it.date, + focusSessions = it.focusSessions, + completed = it.completed, +/* focusTime = it.focusTime, + shortBreakTime = it.shortBreakTime, + longBreakTime = it.longBreakTime,*/ + type = it.type, + consumedFocusTime = it.consumedFocusTime, + consumedShortBreakTime = it.consumedShortBreakTime, + consumedLongBreakTime = it.consumedLongBreakTime, + inProgressTask = it.inProgressTask, + currentCycle = it.currentCycle, + active = it.active + ) + } + } + + override suspend fun updateTask(task: Task) { + task.toTaskEntity().let { + dbQuery.updateTask( + id = it.id, + name = it.name, + description = it.description, + start = it.start, + // end = it.end, + color = it.color, + current = it.current, + date = it.date, + focusSessions = it.focusSessions, + completed = it.completed, +/* focusTime = it.focusTime, + shortBreakTime = it.shortBreakTime, + longBreakTime = it.longBreakTime,*/ + active = it.active + ) + } + } + + override suspend fun deleteTask(id: Int) { + dbQuery.deleteTaskById(id) + } + + override suspend fun deleteAllTasks() { + dbQuery.deleteAllTasks() + } + + override suspend fun updateConsumedFocusTime(id: Int, focusTime: Long) { + dbQuery.updateConsumedFocusTime(id = id, consumedFocusTime = focusTime) + } + + override suspend fun updateConsumedShortBreakTime(id: Int, shortBreakTime: Long) { + dbQuery.updateConsumedShortBreakTime(id = id, consumedShortBreakTime = shortBreakTime) + } + + override suspend fun updateConsumedLongBreakTime(id: Int, longBreakTime: Long) { + dbQuery.updateConsumedLongBreakTime(id = id, consumedLongBreakTime = longBreakTime) + } + + override suspend fun updateTaskInProgress(id: Int, inProgressTask: Boolean) { + dbQuery.updateInProgressTask(id = id, inProgressTask = inProgressTask) + } + + override suspend fun updateTaskCompleted(id: Int, completed: Boolean) { + dbQuery.updateTaskCompleted(id = id, completed = completed) + } + + override suspend fun updateCurrentSessionName(id: Int, current: String) { + dbQuery.updateCurrentSessionName(id = id, current = current) + } + + override suspend fun updateTaskCycleNumber(id: Int, cycle: Int) { + dbQuery.updateTaskCycleNumber(id = id, currentCycle = cycle) + } + + override fun getActiveTask(): Flow { + return dbQuery + .getActiveTask() + .asFlow() + .mapToOneNotNull() + .map { taskEntity -> + taskEntity.toTask() + } + } + + override suspend fun updateTaskActive(id: Int, active: Boolean) { + println("repo: updateTaskActive: $id, $active") + dbQuery.updateTaskActiveStatus(id = id, active = active) + } + + override suspend fun updateAllTasksActiveStatusToInactive() { + dbQuery.updateAllTasksActiveStatusToInactive() + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/SessionType.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/SessionType.kt new file mode 100644 index 0000000..7499a06 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/SessionType.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.domain.model + +sealed class SessionType { + data object Focus : SessionType() + data object ShortBreak : SessionType() + data object LongBreak : SessionType() +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/Task.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/Task.kt new file mode 100644 index 0000000..cbb7eaa --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/Task.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.domain.model + +import kotlinx.datetime.LocalDateTime + +data class Task( + val id: Int = 0, + val name: String, + val description: String? = null, + val type: String, + val start: LocalDateTime, + val color: Long, + val current: String, + val date: LocalDateTime, + val focusSessions: Int, + val currentCycle: Int, + val completed: Boolean, + val consumedFocusTime: Long, + val consumedShortBreakTime: Long, + val consumedLongBreakTime: Long, + val inProgressTask: Boolean, + val active: Boolean +) diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/TaskType.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/TaskType.kt new file mode 100644 index 0000000..5376696 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/TaskType.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.domain.model + +data class TaskType( + val name: String, + val icon: String, + val color: Long +) { + override fun toString(): String { + return name + } +} + +val taskTypes = listOf( + TaskType( + name = "Work", + icon = "work.xml", + color = 0xFF3375fd + ), + TaskType( + name = "Study", + icon = "study.xml", + color = 0xFFff686d + ), + TaskType( + name = "Personal", + icon = "personal.xml", + color = 0xFF24c469 + ), + TaskType( + name = "Other", + icon = "other.xml", + color = 0xFF734efe + ) +) diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/TextFieldState.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/TextFieldState.kt new file mode 100644 index 0000000..73f1114 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/model/TextFieldState.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.domain.model + +data class TextFieldState( + val text: String = "", + val error: String? = null +) diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/repository/settings/SettingsRepository.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/repository/settings/SettingsRepository.kt new file mode 100644 index 0000000..aa7a315 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/repository/settings/SettingsRepository.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.domain.repository.settings + +import kotlinx.coroutines.flow.Flow + +interface SettingsRepository { + suspend fun saveAppTheme(theme: Int) + fun getAppTheme(): Flow + fun clearAll() + fun getSessionTime(): Flow + fun getShortBreakTime(): Flow + fun getLongBreakTime(): Flow + fun getHourFormat(): Flow + fun saveSessionTime(sessionTime: Int) + fun saveLongBreakTime(longBreakTime: Int) + fun saveHourFormat(timeFormat: Int) + fun saveShortBreakTime(shortBreakTime: Int) + fun shortBreakColor(): Flow + fun saveShortBreakColor(color: Long) + fun longBreakColor(): Flow + fun saveLongBreakColor(color: Long) + fun focusColor(): Flow + fun saveFocusColor(color: Long) + fun saveUsername(value: String) + fun getUsername(): Flow +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/repository/tasks/TasksRepository.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/repository/tasks/TasksRepository.kt new file mode 100644 index 0000000..0868a72 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/domain/repository/tasks/TasksRepository.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.domain.repository.tasks + +import com.joelkanyi.focusbloom.core.domain.model.Task +import kotlinx.coroutines.flow.Flow + +interface TasksRepository { + fun getTasks(): Flow> + fun getTask(id: Int): Flow + suspend fun addTask(task: Task) + suspend fun updateTask(task: Task) + suspend fun deleteTask(id: Int) + suspend fun deleteAllTasks() + suspend fun updateConsumedFocusTime(id: Int, focusTime: Long) + suspend fun updateConsumedShortBreakTime(id: Int, shortBreakTime: Long) + suspend fun updateConsumedLongBreakTime(id: Int, longBreakTime: Long) + suspend fun updateTaskInProgress(id: Int, inProgressTask: Boolean) + suspend fun updateTaskCompleted(id: Int, completed: Boolean) + suspend fun updateCurrentSessionName(id: Int, current: String) + suspend fun updateTaskCycleNumber(id: Int, cycle: Int) + fun getActiveTask(): Flow + suspend fun updateTaskActive(id: Int, active: Boolean) + + suspend fun updateAllTasksActiveStatusToInactive() +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomButton.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomButton.kt index ccc8613..44b42bd 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomButton.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomButton.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.component import androidx.compose.material3.Button @@ -14,15 +29,15 @@ fun BloomButton( onClick: () -> Unit, shape: Shape = MaterialTheme.shapes.medium, backgroundColor: Color = MaterialTheme.colorScheme.primary, - content: @Composable () -> Unit, + content: @Composable () -> Unit ) { Button( modifier = modifier, onClick = onClick, shape = shape, colors = ButtonDefaults.buttonColors( - containerColor = backgroundColor, - ), + containerColor = backgroundColor + ) ) { content() } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomCircleButton.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomCircleButton.kt index ed357f1..691b9ca 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomCircleButton.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomCircleButton.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.component import androidx.compose.foundation.background @@ -17,7 +32,7 @@ fun BloomCircleButton( modifier: Modifier = Modifier, icon: @Composable () -> Unit, onClick: () -> Unit, - color: Color, + color: Color ) { Box( modifier = modifier @@ -27,7 +42,7 @@ fun BloomCircleButton( .clickable { onClick() }, - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { icon() } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomDropDown.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomDropDown.kt index ab462e1..e0c2601 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomDropDown.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomDropDown.kt @@ -1,6 +1,22 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.component import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -9,10 +25,14 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CornerBasedShape -import androidx.compose.material3.ExperimentalMaterial3Api -/*import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults*/ +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -26,9 +46,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.joelkanyi.focusbloom.domain.model.TextFieldState +import com.joelkanyi.focusbloom.core.domain.model.TextFieldState -@OptIn(ExperimentalMaterial3Api::class) @Composable fun BloomDropDown( modifier: Modifier = Modifier, @@ -38,7 +57,7 @@ fun BloomDropDown( selectedOption: TextFieldState, onOptionSelected: (T) -> Unit, textStyle: TextStyle = MaterialTheme.typography.titleSmall, - shape: CornerBasedShape = MaterialTheme.shapes.small, + shape: CornerBasedShape = MaterialTheme.shapes.small ) { var expanded by remember { mutableStateOf(false) } Column { @@ -46,14 +65,6 @@ fun BloomDropDown( label() Spacer(modifier = Modifier.height(4.dp)) } - /*ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { - if (enabled) { - expanded = !expanded - } - }, - ) {*/ Box( modifier = modifier .height(56.dp) @@ -65,50 +76,63 @@ fun BloomDropDown( } else { MaterialTheme.colorScheme.onBackground.copy(alpha = .4f) }, - shape = shape, + shape = shape ) - .clip(shape), - contentAlignment = Alignment.CenterStart, + .clip(shape) + .clickable { + if (enabled) { + expanded = !expanded + } + }, + contentAlignment = Alignment.CenterStart ) { Row( modifier = Modifier .fillMaxWidth() .padding( vertical = 8.dp, - horizontal = 12.dp, + horizontal = 12.dp ), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = selectedOption.text, - style = textStyle, + style = textStyle ) - /*if (enabled) { - ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) - }*/ - // } + if (enabled) { + Icon( + modifier = Modifier + .padding(start = 8.dp) + .size(24.dp), + imageVector = if (expanded) { + Icons.Filled.ArrowDropUp + } else { + Icons.Filled.ArrowDropDown + }, + contentDescription = null + ) + } } - /*ExposedDropdownMenu( + DropdownMenu( expanded = expanded, - onDismissRequest = { expanded = false }, + onDismissRequest = { expanded = false } ) { options.forEach { selectionOption -> DropdownMenuItem( text = { Text( text = selectionOption.toString(), - style = MaterialTheme.typography.labelLarge, + style = MaterialTheme.typography.labelLarge ) }, onClick = { onOptionSelected(selectionOption) expanded = false - }, - contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, + } ) } - }*/ + } } if (!selectedOption.error.isNullOrEmpty()) { Text( @@ -116,7 +140,7 @@ fun BloomDropDown( text = selectedOption.error, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.End, + textAlign = TextAlign.End ) } } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomIncrementer.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomIncrementer.kt index f9d6f01..8629ee2 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomIncrementer.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomIncrementer.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.component import androidx.compose.foundation.layout.Arrangement @@ -20,30 +35,30 @@ fun BloomIncrementer( modifier: Modifier = Modifier, onClickRemove: () -> Unit, onClickAdd: () -> Unit, - currentValue: Int, + currentValue: Int ) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, + horizontalArrangement = Arrangement.Center ) { BloomCircleButton( icon = { Icon( imageVector = Icons.Filled.Remove, contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimary, + tint = MaterialTheme.colorScheme.onPrimary ) }, onClick = onClickRemove, - color = MaterialTheme.colorScheme.primary, + color = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.width(12.dp)) Text( text = "$currentValue", - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleLarge ) Spacer(modifier = Modifier.width(12.dp)) @@ -53,11 +68,11 @@ fun BloomIncrementer( Icon( imageVector = Icons.Filled.Add, contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimary, + tint = MaterialTheme.colorScheme.onPrimary ) }, onClick = onClickAdd, - color = MaterialTheme.colorScheme.primary, + color = MaterialTheme.colorScheme.primary ) } } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomInputTextField.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomInputTextField.kt index c546449..850e5bb 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomInputTextField.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomInputTextField.kt @@ -1,18 +1,48 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.component +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField 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.text.TextStyle +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.joelkanyi.focusbloom.domain.model.TextFieldState +import com.joelkanyi.focusbloom.core.domain.model.TextFieldState @Composable fun BloomInputTextField( @@ -22,21 +52,23 @@ fun BloomInputTextField( leadingIcon: (@Composable () -> Unit)? = null, trailingIcon: (@Composable () -> Unit)? = null, value: TextFieldState, + maxLines: Int = 1, + editable: Boolean = true, onValueChange: (String) -> Unit, textStyle: TextStyle = MaterialTheme.typography.titleSmall, shape: CornerBasedShape = MaterialTheme.shapes.small, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default ) { Column( - modifier = modifier, + modifier = modifier ) { if (label != null) { label() Spacer(modifier = Modifier.height(4.dp)) } OutlinedTextField( - modifier = Modifier - .height(56.dp), + modifier = Modifier.fillMaxWidth() + .defaultMinSize(minWidth = 56.dp), value = value.text, onValueChange = onValueChange, placeholder = placeholder, @@ -44,14 +76,87 @@ fun BloomInputTextField( trailingIcon = trailingIcon, textStyle = textStyle, shape = shape, + maxLines = maxLines, + singleLine = maxLines == 1, keyboardOptions = keyboardOptions, + readOnly = !editable ) if (!value.error.isNullOrEmpty()) { Text( text = value.error, style = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.error, - ), + color = MaterialTheme.colorScheme.error + ) + ) + } + } +} + +@Composable +fun BloomDateBoxField( + modifier: Modifier = Modifier, + label: (@Composable () -> Unit)? = null, + enabled: Boolean = true, + currentTextState: TextFieldState, + onClick: () -> Unit, + textStyle: TextStyle = MaterialTheme.typography.titleSmall, + shape: CornerBasedShape = MaterialTheme.shapes.small +) { + Column { + if (label != null) { + label() + Spacer(modifier = Modifier.height(4.dp)) + } + Box( + modifier = modifier + .height(56.dp) + .border( + width = 1.dp, + color = if (enabled) { + MaterialTheme.colorScheme.onBackground + } else { + MaterialTheme.colorScheme.onBackground.copy(alpha = .4f) + }, + shape = shape + ) + .clip(shape) + .clickable { + onClick() + }, + contentAlignment = Alignment.CenterStart + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + vertical = 8.dp, + horizontal = 12.dp + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = currentTextState.text, + style = textStyle + ) + if (enabled) { + Icon( + modifier = Modifier + .padding(start = 8.dp) + .size(24.dp), + imageVector = Icons.Default.DateRange, + contentDescription = null + ) + } + } + } + if (!currentTextState.error.isNullOrEmpty()) { + Text( + modifier = Modifier.fillMaxWidth(), + text = currentTextState.error, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.End ) } } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomNavigationRailBar.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomNavigationRailBar.kt index 988ec54..cd226cd 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomNavigationRailBar.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomNavigationRailBar.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.component import androidx.compose.foundation.layout.fillMaxHeight @@ -19,7 +34,7 @@ import com.joelkanyi.focusbloom.core.presentation.utils.FilledIcon fun BloomNavigationRailBar( modifier: Modifier = Modifier, tabNavigator: TabNavigator, - navRailItems: List, + navRailItems: List ) { NavigationRail( modifier = modifier.fillMaxHeight().alpha(0.95F), @@ -32,7 +47,7 @@ fun BloomNavigationRailBar( contentDescription = "Logo", )*/ }, - contentColor = MaterialTheme.colorScheme.onSurface, + contentColor = MaterialTheme.colorScheme.onSurface ) { navRailItems.forEach { item -> val isSelected = tabNavigator.current == item @@ -46,15 +61,15 @@ fun BloomNavigationRailBar( } else { it }, - contentDescription = item.options.title, + contentDescription = item.options.title ) } }, label = { Text(text = item.options.title) }, alwaysShowLabel = true, selected = tabNavigator.current == item, - onClick = { tabNavigator.current = item }, + onClick = { tabNavigator.current = item } ) } } -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTab.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTab.kt index ef9b72a..1b2cbb4 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTab.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTab.kt @@ -1,21 +1,34 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.component import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions -import com.joelkanyi.focusbloom.calendar.CalendarScreen -import com.joelkanyi.focusbloom.home.HomeScreen -import com.joelkanyi.focusbloom.settings.SettingsScreen -import com.joelkanyi.focusbloom.statistics.StatisticsScreen -import com.joelkanyi.focusbloom.task.AddTaskScreen +import com.joelkanyi.focusbloom.feature.addtask.AddTaskScreen +import com.joelkanyi.focusbloom.feature.calendar.CalendarScreen +import com.joelkanyi.focusbloom.feature.home.HomeScreen +import com.joelkanyi.focusbloom.feature.settings.SettingsScreen +import com.joelkanyi.focusbloom.feature.statistics.StatisticsScreen import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource internal sealed class BloomTab { - internal object HomeTab : Tab, Screen { + internal object HomeTab : Tab { @OptIn(ExperimentalResourceApi::class) override val options: TabOptions @Composable @@ -27,14 +40,14 @@ internal sealed class BloomTab { TabOptions( index = 0u, title = title, - icon = icon, + icon = icon ) } } @Composable override fun Content() { - Navigator(HomeScreen()) + HomeScreen() } } @@ -50,14 +63,14 @@ internal sealed class BloomTab { TabOptions( index = 1u, title = title, - icon = icon, + icon = icon ) } } @Composable override fun Content() { - Navigator(CalendarScreen()) + CalendarScreen() } } @@ -73,14 +86,14 @@ internal sealed class BloomTab { TabOptions( index = 2u, title = title, - icon = icon, + icon = icon ) } } @Composable override fun Content() { - Navigator(StatisticsScreen()) + StatisticsScreen() } } @@ -96,14 +109,14 @@ internal sealed class BloomTab { TabOptions( index = 3u, title = title, - icon = icon, + icon = icon ) } } @Composable override fun Content() { - Navigator(SettingsScreen()) + SettingsScreen() } } @@ -119,14 +132,14 @@ internal sealed class BloomTab { TabOptions( index = 4u, title = title, - icon = icon, + icon = icon ) } } @Composable override fun Content() { - Navigator(AddTaskScreen()) + AddTaskScreen() } } } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTimerControls.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTimerControls.kt index cf54f2f..7aa981b 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTimerControls.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTimerControls.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.component import androidx.compose.foundation.layout.Arrangement @@ -5,6 +20,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Replay import androidx.compose.material.icons.filled.SkipNext import androidx.compose.material3.Icon @@ -14,24 +30,27 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.joelkanyi.focusbloom.feature.taskprogress.TimerState @Composable fun BloomTimerControls( modifier: Modifier = Modifier, + state: TimerState, onClickReset: () -> Unit, onClickNext: () -> Unit, - onClickAction: () -> Unit, + onClickAction: (state: TimerState) -> Unit ) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceAround, + horizontalArrangement = Arrangement.SpaceAround ) { IconButton(onClick = onClickReset) { Icon( modifier = Modifier.size(120.dp), imageVector = Icons.Filled.Replay, contentDescription = "Reset Timer", + tint = MaterialTheme.colorScheme.onPrimary ) } @@ -40,13 +59,28 @@ fun BloomTimerControls( icon = { Icon( modifier = Modifier.size(48.dp), - imageVector = Icons.Filled.Pause, + imageVector = when (state) { + TimerState.Paused -> { + Icons.Filled.PlayArrow + } + TimerState.Ticking -> { + Icons.Filled.Pause + } + TimerState.Finished -> { + Icons.Filled.Replay + } + else -> { + Icons.Filled.PlayArrow + } + }, contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimary, + tint = MaterialTheme.colorScheme.onPrimary ) }, - onClick = onClickAction, - color = MaterialTheme.colorScheme.primary, + onClick = { + onClickAction(state) + }, + color = MaterialTheme.colorScheme.primary ) IconButton(onClick = onClickNext) { @@ -54,6 +88,7 @@ fun BloomTimerControls( modifier = Modifier.size(120.dp), imageVector = Icons.Filled.SkipNext, contentDescription = "Next Timer", + tint = MaterialTheme.colorScheme.onPrimary ) } } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTopAppBar.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTopAppBar.kt index 1ceebad..4d9a727 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTopAppBar.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/BloomTopAppBar.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.component import androidx.compose.material3.ExperimentalMaterial3Api @@ -13,24 +28,28 @@ import androidx.compose.ui.Modifier fun BloomTopAppBar( modifier: Modifier = Modifier, hasBackNavigation: Boolean = false, - actions: @Composable () -> Unit = {}, - navigationIcon: @Composable () -> Unit = {}, - colors: TopAppBarColors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.background, + actions: (@Composable () -> Unit)? = null, + navigationIcon: (@Composable () -> Unit)? = null, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background ), - title: @Composable () -> Unit = {}, + title: @Composable () -> Unit = {} ) { TopAppBar( modifier = modifier, title = title, actions = { - actions() + if (actions != null) { + actions() + } }, navigationIcon = { if (hasBackNavigation) { - navigationIcon() + if (navigationIcon != null) { + navigationIcon() + } } }, - colors = colors, + colors = colors ) } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/TaskCard.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/TaskCard.kt index 0f326be..e18351b 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/TaskCard.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/TaskCard.kt @@ -1,6 +1,21 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.component -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -16,7 +31,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -26,7 +40,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -37,64 +50,82 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.joelkanyi.focusbloom.domain.model.Task +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.utils.calculateEndTime +import com.joelkanyi.focusbloom.core.utils.durationInMinutes +import com.joelkanyi.focusbloom.core.utils.prettyTimeDifference +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalResourceApi::class) @Composable fun TaskCard( task: Task, + focusSessions: Int, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int, + hourFormat: Int, onClick: (task: Task) -> Unit, + onShowTaskOption: (task: Task) -> Unit ) { - var showTaskOption by remember { - mutableStateOf(false) + val end by remember { + mutableStateOf( + task.start.calculateEndTime( + focusSessions = focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + ) } Card( modifier = Modifier .fillMaxWidth(), onClick = { onClick(task) - }, + } ) { Column( modifier = Modifier .padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { Row( modifier = Modifier .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.SpaceBetween ) { Column( modifier = Modifier .fillMaxWidth(.85f), - verticalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( text = task.name, style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.Bold, - fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp ), maxLines = 2, - overflow = TextOverflow.Ellipsis, + overflow = TextOverflow.Ellipsis ) if (task.description != null) { Text( - text = task.description!!, + text = task.description, style = MaterialTheme.typography.bodyMedium, maxLines = 3, - overflow = TextOverflow.Ellipsis, + overflow = TextOverflow.Ellipsis ) } } Icon( modifier = Modifier .clickable { - showTaskOption = !showTaskOption + onShowTaskOption(task) }, imageVector = Icons.Filled.MoreVert, - contentDescription = "Task Options", + contentDescription = "Task Options" ) } @@ -102,7 +133,7 @@ fun TaskCard( modifier = Modifier .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { Column { Text( @@ -110,89 +141,59 @@ fun TaskCard( withStyle( style = SpanStyle( fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, - ), + fontSize = 18.sp + ) ) { - append("${task.current}") + append("${task.currentCycle}") } - append("/${task.taskCycles()}") - }, + append("/${task.focusSessions}") + } ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "${task.durationInMinutes()} minutes", - style = MaterialTheme.typography.bodySmall, - ) - } - Box( - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = Icons.Filled.PlayArrow, - contentDescription = "Task Options", - tint = MaterialTheme.colorScheme.onPrimary, + text = "${ + task.durationInMinutes( + focusSessions = focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + } minutes", + style = MaterialTheme.typography.bodySmall ) - } - } - - AnimatedVisibility(visible = showTaskOption) { - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { + Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Delete", - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.SemiBold, + text = prettyTimeDifference( + start = task.start, + end = end, + timeFormat = hourFormat ), + style = MaterialTheme.typography.bodySmall + ) + } + if (task.completed) { + Image( + modifier = Modifier + .size(48.dp), + painter = painterResource("ic_complete.xml"), + contentDescription = "Task Options" ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), + } else { + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.Center ) { - Text( - text = "Cancel", - style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.SemiBold, - ), + Icon( + imageVector = Icons.Filled.PlayArrow, + contentDescription = "Task Options", + tint = MaterialTheme.colorScheme.onPrimary ) - Button( - shape = MaterialTheme.shapes.medium, - onClick = { /*TODO*/ }, - ) { - Text( - text = "Save", - ) - } } } } } } } - -fun Task.durationInMinutes(): Int { - /** - * Difference between start and end time in minutes - * They are in LocalDateTime format - */ - // return ChronoUnit.MINUTES.between(this.start, this.end).toInt() - return 25 -} - -fun Task.taskCycles(): Int { - /** - * A Focus Session Task 25 minutes - * A Short Break 5 minutes - * A Long Break 15 minutes - * A Task Cycle is a Focus Session Task + Short Break + Long Break - */ - return this.durationInMinutes() / 25 -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/TaskProgress.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/TaskProgress.kt index 247d7dc..d705e59 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/TaskProgress.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/component/TaskProgress.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.component import androidx.compose.animation.core.animateFloatAsState @@ -33,9 +48,10 @@ fun TaskProgress( percentage: Float, radius: Dp = 20.dp, mainColor: Color, + counterColor: Color, strokeWidth: Dp = 8.dp, animationDuration: Int = 800, - animDelay: Int = 0, + animDelay: Int = 0 ) { var animationPlayed by remember { mutableStateOf(false) @@ -45,9 +61,9 @@ fun TaskProgress( targetValue = if (animationPlayed) percentage else 0f, animationSpec = tween( durationMillis = animationDuration, - delayMillis = animDelay, + delayMillis = animDelay ), - label = "", + label = "" ) LaunchedEffect(key1 = true) { @@ -55,18 +71,18 @@ fun TaskProgress( } Box( - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { Canvas( modifier = Modifier - .size(radius * 5f), + .size(radius * 5f) ) { drawArc( color = Color.LightGray, startAngle = 0f, sweepAngle = 360f, useCenter = false, - style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round), + style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round) ) drawArc( @@ -74,7 +90,7 @@ fun TaskProgress( startAngle = -360f, sweepAngle = (360 * (currentPercentage.value * 0.01)).toFloat(), useCenter = false, - style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round), + style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round) ) } @@ -82,24 +98,25 @@ fun TaskProgress( contentAlignment = Alignment.Center, modifier = Modifier .size(110.dp) - .clip(CircleShape), + .clip(CircleShape) ) { Column( modifier = Modifier.padding(8.dp), verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + horizontalAlignment = Alignment.CenterHorizontally ) { - if (content?.isNullOrEmpty()?.not() == true) { + if (content?.isEmpty()?.not() == true) { Text( text = content, fontSize = 22.sp, - fontWeight = FontWeight.Bold, + fontWeight = FontWeight.Bold ) } else { Text( text = "${(currentPercentage.value).toInt()}%", fontSize = 22.sp, fontWeight = FontWeight.Bold, + color = counterColor ) } } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Colors.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Colors.kt index d8ead8e..0248604 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Colors.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Colors.kt @@ -1,25 +1,56 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.theme import androidx.compose.ui.graphics.Color val PrimaryColor = Color(0xff214E78) -val PrimaryLightColor = PrimaryColor.copy(alpha = 0.95f) +val PrimaryLightColor = PrimaryColor.copy(alpha = 0.75f) val SecondaryColor = Color(0xff7C93BE) -val SecondaryLightColor = SecondaryColor.copy(alpha = 0.95f) +val SecondaryLightColor = SecondaryColor.copy(alpha = 0.75f) val PrimaryTextColor = Color(0xffffffff) val SecondaryTextColor = Color(0xff000000) -val SurfaceDark = Color(0xFF42474e) +// val SurfaceDark = Color(0xFF1f1f1f) +// val SurfaceDark = Color(0xFF121212) +val SurfaceDark = Color(0xFF161616) + val SurfaceLight = Color(0xFFFFFFFF) val BackgroundLightColor = Color(0xffF1F0F5) -val BackgroundDarkColor = Color(0xff121212) + +val BackgroundDarkColor = Color(0xff010100) val ErrorColor = Color(0xFFFF8989) val OnErrorColor = Color(0xFF000000) -val SessionColor = Color(0xFfBA4949) -val ShortBreakColor = Color(0xFf38858A) -val LongBreakColor = Color(0xFf397097) +val SuccessColor = Color(0xFF34b233) + +const val SessionColor = 0xFfBA4949 +const val ShortBreakColor = 0xFf38858A +const val LongBreakColor = 0xFf397097 + +const val Red = 0xFFFF0000 +const val Orange = 0xFFFFA500 +const val Blue = 0xFF0000FF +const val Green = 0xFF00FF00 + +const val LightGreen = 0xFF90EE90 +const val Yellow = 0xFFFFFF00 +const val LightBlue = 0xFFADD8E6 +const val Pink = 0xFFFFC0CB diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Shapes.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Shapes.kt index ed150d3..d79b5ca 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Shapes.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Shapes.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.theme import androidx.compose.foundation.shape.RoundedCornerShape @@ -7,5 +22,5 @@ import androidx.compose.ui.unit.dp internal val Shapes = Shapes( small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(8.dp), - large = RoundedCornerShape(12.dp), + large = RoundedCornerShape(12.dp) ) diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Theme.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Theme.kt index 3149ce7..a0045d0 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Theme.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Theme.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.theme import androidx.compose.foundation.isSystemInDarkTheme @@ -23,7 +38,7 @@ private val LightColors = lightColorScheme( secondaryContainer = PrimaryColor, onSecondaryContainer = Color.White, error = ErrorColor, - onError = OnErrorColor, + onError = OnErrorColor ) private val DarkColors = darkColorScheme( @@ -42,13 +57,13 @@ private val DarkColors = darkColorScheme( secondaryContainer = PrimaryColor, onSecondaryContainer = Color.White, error = ErrorColor, - onError = OnErrorColor, + onError = OnErrorColor ) @Composable internal fun FocusBloomTheme( useDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit, + content: @Composable () -> Unit ) { val autoColors = if (useDarkTheme) DarkColors else LightColors @@ -56,6 +71,6 @@ internal fun FocusBloomTheme( colorScheme = autoColors, typography = getTypography(), shapes = Shapes, - content = content, + content = content ) } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Type.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Type.kt index f5e00f9..d452888 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Type.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/theme/Type.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.theme import androidx.compose.material3.Typography @@ -16,7 +31,7 @@ internal fun getTypography(): Typography { "Montserrat", "montserrat_regular", FontWeight.Normal, - FontStyle.Normal, + FontStyle.Normal ) val montserratBold = @@ -24,7 +39,7 @@ internal fun getTypography(): Typography { "Montserrat", "montserrat_bold", FontWeight.Bold, - FontStyle.Normal, + FontStyle.Normal ) val montserratLight = @@ -32,7 +47,7 @@ internal fun getTypography(): Typography { "Montserrat", "montserrat_light", FontWeight.Light, - FontStyle.Normal, + FontStyle.Normal ) val montserratMedium = @@ -40,7 +55,7 @@ internal fun getTypography(): Typography { "Montserrat", "montserrat_medium", FontWeight.Medium, - FontStyle.Normal, + FontStyle.Normal ) val montserratSemiBold = @@ -48,7 +63,7 @@ internal fun getTypography(): Typography { "Montserrat", "montserrat_semi_bold", FontWeight.SemiBold, - FontStyle.Normal, + FontStyle.Normal ) val montserratThin = @@ -56,7 +71,7 @@ internal fun getTypography(): Typography { "Montserrat", "montserrat_thin", FontWeight.Thin, - FontStyle.Normal, + FontStyle.Normal ) val montserratExtraBold = @@ -64,7 +79,7 @@ internal fun getTypography(): Typography { "Montserrat", "montserrat_extrabold", FontWeight.ExtraBold, - FontStyle.Normal, + FontStyle.Normal ) val montserratExtraLight = @@ -72,13 +87,13 @@ internal fun getTypography(): Typography { "Montserrat", "montserrat_extralight", FontWeight.ExtraLight, - FontStyle.Normal, + FontStyle.Normal ) val montserratBlack = font( "Montserrat", "montserrat_black", FontWeight.Black, - FontStyle.Normal, + FontStyle.Normal ) @Composable @@ -91,107 +106,107 @@ internal fun getTypography(): Typography { montserratSemiBold, montserratBold, montserratExtraBold, - montserratBlack, + montserratBlack ) return Typography( displayLarge = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W400, - fontSize = 50.sp, - lineHeight = 64.sp, - letterSpacing = (-0.25).sp, + fontSize = 50.sp + // lineHeight = 64.sp, + // letterSpacing = (-0.25).sp, ), displayMedium = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W400, - fontSize = 40.sp, - lineHeight = 52.sp, + fontSize = 40.sp + // lineHeight = 52.sp, ), displaySmall = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W400, - fontSize = 30.sp, - lineHeight = 44.sp, + fontSize = 30.sp + // lineHeight = 44.sp, ), headlineLarge = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W400, - fontSize = 28.sp, - lineHeight = 40.sp, + fontSize = 28.sp + // lineHeight = 40.sp, ), headlineMedium = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W400, - fontSize = 24.sp, - lineHeight = 36.sp, + fontSize = 24.sp + // lineHeight = 36.sp, ), headlineSmall = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W400, - fontSize = 20.sp, - lineHeight = 32.sp, + fontSize = 20.sp + // lineHeight = 32.sp, ), titleLarge = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W700, - fontSize = 18.sp, - lineHeight = 28.sp, + fontSize = 18.sp + // lineHeight = 28.sp, ), titleMedium = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W700, - fontSize = 14.sp, - lineHeight = 24.sp, - letterSpacing = 0.1.sp, + fontSize = 14.sp + // lineHeight = 24.sp, + // letterSpacing = 0.1.sp, ), titleSmall = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W500, - fontSize = 12.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp, + fontSize = 12.sp + // lineHeight = 20.sp, + // letterSpacing = 0.1.sp, ), bodyLarge = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W400, - fontSize = 14.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp, + fontSize = 14.sp + // lineHeight = 24.sp, + // letterSpacing = 0.5.sp, ), bodyMedium = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W400, - fontSize = 12.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp, + fontSize = 12.sp + // lineHeight = 20.sp, + // letterSpacing = 0.25.sp, ), bodySmall = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W400, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp, + fontSize = 11.sp + // lineHeight = 16.sp, + // letterSpacing = 0.4.sp, ), labelLarge = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W400, - fontSize = 13.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp, + fontSize = 13.sp + // lineHeight = 20.sp, + // letterSpacing = 0.1.sp, ), labelMedium = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W400, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp, + fontSize = 11.sp + // lineHeight = 16.sp, + // letterSpacing = 0.5.sp, ), labelSmall = TextStyle( fontFamily = montserrat(), fontWeight = FontWeight.W500, - fontSize = 9.sp, - lineHeight = 16.sp, - ), + fontSize = 9.sp + // lineHeight = 16.sp, + ) ) } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/utils/FilledIcon.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/utils/FilledIcon.kt index 918e323..11cb1e3 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/utils/FilledIcon.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/presentation/utils/FilledIcon.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.presentation.utils import androidx.compose.runtime.Composable diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/samples/Samples.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/samples/Samples.kt deleted file mode 100644 index eabf21e..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/samples/Samples.kt +++ /dev/null @@ -1,209 +0,0 @@ -package com.joelkanyi.focusbloom.core.samples - -import com.joelkanyi.focusbloom.domain.model.Task -import kotlinx.datetime.LocalDateTime - -val sampleTasks = listOf( - Task( - name = "Google I/O Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2021-05-18T00:00:00"), - end = LocalDateTime.parse("2021-05-18T01:00:00"), - date = LocalDateTime.parse("2021-05-18T00:00:00"), - current = 1, - description = "Tune in to find out about how we're furthering our mission to organize the world’s information and make it universally accessible and useful.", - ), - Task( - name = "Google I/O Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2021-05-18T02:00:00"), - end = LocalDateTime.parse("2021-05-18T04:00:00"), - date = LocalDateTime.parse("2021-05-18T00:00:00"), - current = 1, - description = "Tune in to find out about how we're furthering our mission to organize the world’s information and make it universally accessible and useful.", - ), - Task( - name = "Google I/O Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2021-05-18T06:00:00"), - end = LocalDateTime.parse("2021-05-18T07:00:00"), - date = LocalDateTime.parse("2021-05-18T00:00:00"), - current = 1, - description = "Tune in to find out about how we're furthering our mission to organize the world’s information and make it universally accessible and useful.", - ), - Task( - name = "Google I/O Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2021-05-18T09:00:00"), - end = LocalDateTime.parse("2021-05-18T11:00:00"), - date = LocalDateTime.parse("2021-05-18T00:00:00"), - current = 1, - description = "Tune in to find out about how we're furthering our mission to organize the world’s information and make it universally accessible and useful.", - ), - Task( - name = "Developer Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2021-05-18T09:00:00"), - end = LocalDateTime.parse("2021-05-18T10:00:00"), - date = LocalDateTime.parse("2021-05-18T00:00:00"), - current = 1, - description = "Learn about the latest updates to our developer products and platforms from Google Developers.", - ), - Task( - name = "What's new in Android", - color = 0xFF1B998B, - start = LocalDateTime.parse("2021-05-18T10:00:00"), - end = LocalDateTime.parse("2021-05-18T11:00:00"), - date = LocalDateTime.parse("2021-05-18T00:00:00"), - current = 1, - description = "In this Keynote, Chet Haase, Dan Sandler, and Romain Guy discuss the latest Android features and enhancements for developers.", - ), - Task( - name = "What's new in Material Design", - color = 0xFF6DD3CE, - start = LocalDateTime.parse("2021-05-18T11:00:00"), - end = LocalDateTime.parse("2021-05-18T11:45:00"), - date = LocalDateTime.parse("2021-05-18T00:00:00"), - current = 1, - description = "Learn about the latest design improvements to help you build personal dynamic experiences with Material Design.", - ), - Task( - name = "What's new in Machine Learning", - color = 0xFFF4BFDB, - start = LocalDateTime.parse("2021-05-18T10:00:00"), - end = LocalDateTime.parse("2021-05-18T11:00:00"), - date = LocalDateTime.parse("2021-05-18T00:00:00"), - current = 1, - description = "Learn about the latest and greatest in ML from Google. We’ll cover what’s available to developers when it comes to creating, understanding, and deploying models for a variety of different applications.", - ), - Task( - name = "What's new in Machine Learning", - color = 0xFFF4BFDB, - start = LocalDateTime.parse("2021-05-18T10:30:00"), - end = LocalDateTime.parse("2021-05-18T11:30:00"), - date = LocalDateTime.parse("2021-05-18T00:00:00"), - current = 1, - description = "Learn about the latest and greatest in ML from Google. We’ll cover what’s available to developers when it comes to creating, understanding, and deploying models for a variety of different applications.", - ), - Task( - name = "Jetpack Compose Basics", - color = 0xFF1B998B, - start = LocalDateTime.parse("2021-05-20T12:00:00"), - end = LocalDateTime.parse("2021-05-20T13:00:00"), - date = LocalDateTime.parse("2021-05-20T00:00:00"), - current = 1, - description = "This Workshop will take you through the basics of building your first app with Jetpack Compose, Android's new modern UI toolkit that simplifies and accelerates UI development on Android.", - ), - /** - * Today tasks - 2023-09-02 - */ - Task( - name = "Google I/O Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2023-09-02T00:00:00"), - end = LocalDateTime.parse("2023-09-02T01:00:00"), - date = LocalDateTime.parse("2023-09-02T00:00:00"), - current = 1, - description = "Tune in to find out about how we're furthering our mission to organize the world’s information and make it universally accessible and useful.", - ), - Task( - name = "Google I/O Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2023-09-02T02:00:00"), - end = LocalDateTime.parse("2023-09-02T04:00:00"), - date = LocalDateTime.parse("2023-09-02T00:00:00"), - current = 1, - description = "Tune in to find out about how we're furthering our mission to organize the world’s information and make it universally accessible and useful.", - ), - Task( - name = "Google I/O Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2023-09-02T06:00:00"), - end = LocalDateTime.parse("2023-09-02T07:00:00"), - date = LocalDateTime.parse("2023-09-02T00:00:00"), - current = 1, - description = "Tune in to find out about how we're furthering our mission to organize the world’s information and make it universally accessible and useful.", - ), - Task( - name = "Google I/O Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2023-09-02T09:00:00"), - end = LocalDateTime.parse("2023-09-02T11:00:00"), - date = LocalDateTime.parse("2023-09-02T00:00:00"), - current = 1, - description = "Tune in to find out about how we're furthering our mission to organize the world’s information and make it universally accessible and useful.", - ), - Task( - name = "Developer Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2023-09-02T09:00:00"), - end = LocalDateTime.parse("2023-09-02T10:00:00"), - date = LocalDateTime.parse("2023-09-02T00:00:00"), - current = 1, - description = "Learn about the latest updates to our developer products and platforms from Google Developers.", - ), - Task( - name = "What's new in Android", - color = 0xFF1B998B, - start = LocalDateTime.parse("2023-09-02T10:00:00"), - end = LocalDateTime.parse("2023-09-02T11:00:00"), - date = LocalDateTime.parse("2023-09-02T00:00:00"), - current = 1, - description = "In this Keynote, Chet Haase, Dan Sandler, and Romain Guy discuss the latest Android features and enhancements for developers.", - ), - Task( - name = "What's new in Material Design", - color = 0xFF6DD3CE, - start = LocalDateTime.parse("2023-09-02T11:00:00"), - end = LocalDateTime.parse("2023-09-02T11:45:00"), - date = LocalDateTime.parse("2023-09-02T00:00:00"), - current = 1, - description = "Learn about the latest design improvements to help you build personal dynamic experiences with Material Design.", - ), - Task( - name = "What's new in Machine Learning", - color = 0xFFF4BFDB, - start = LocalDateTime.parse("2023-09-02T10:00:00"), - end = LocalDateTime.parse("2023-09-02T11:00:00"), - date = LocalDateTime.parse("2023-09-02T00:00:00"), - current = 1, - description = "Learn about the latest and greatest in ML from Google. We’ll cover what’s available to developers when it comes to creating, understanding, and deploying models for a variety of different applications.", - ), - Task( - name = "What's new in Machine Learning", - color = 0xFFF4BFDB, - start = LocalDateTime.parse("2023-09-02T10:30:00"), - end = LocalDateTime.parse("2023-09-02T11:30:00"), - date = LocalDateTime.parse("2023-09-02T00:00:00"), - current = 1, - description = "Learn about the latest and greatest in ML from Google. We’ll cover what’s available to developers when it comes to creating, understanding, and deploying models for a variety of different applications.", - ), - // Today tasks - 2023-09-03 - Task( - name = "Google I/O Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2023-09-03T00:00:00"), - end = LocalDateTime.parse("2023-09-03T01:00:00"), - date = LocalDateTime.parse("2023-09-03T00:00:00"), - current = 1, - description = "Tune in to find out about how we're furthering our mission to organize the world’s information and make it universally accessible and useful.", - ), - Task( - name = "Google I/O Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2023-09-03T02:00:00"), - end = LocalDateTime.parse("2023-09-03T04:00:00"), - date = LocalDateTime.parse("2023-09-03T00:00:00"), - current = 1, - description = "Tune in to find out about how we're furthering our mission to organize the world’s information and make it universally accessible and useful.", - ), - Task( - name = "Google I/O Keynote", - color = 0xFFAFBBF2, - start = LocalDateTime.parse("2023-09-03T06:00:00"), - end = LocalDateTime.parse("2023-09-03T07:00:00"), - date = LocalDateTime.parse("2023-09-03T00:00:00"), - current = 1, - description = "Tune in to find out about how we're furthering our mission to organize the world’s information and make it universally accessible and useful.", - ), -) diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/utils/UiEvents.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/utils/UiEvents.kt new file mode 100644 index 0000000..616618c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/utils/UiEvents.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.core.utils + +sealed class UiEvents { + data class ShowSnackbar(val message: String) : UiEvents() + data object Navigation : UiEvents() + data object NavigateBack : UiEvents() +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/utils/Utils.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/utils/Utils.kt index a7e53d9..5979564 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/utils/Utils.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/core/utils/Utils.kt @@ -1,97 +1,99 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.core.utils import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ParentDataModifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.capitalize +import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp -import com.joelkanyi.focusbloom.domain.model.Task +import cafe.adriel.voyager.navigator.Navigator +import com.joelkanyi.focusbloom.core.domain.model.SessionType +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.domain.model.taskTypes import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone -import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.atTime import kotlinx.datetime.minus import kotlinx.datetime.plus +import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime +import kotlin.jvm.JvmInline @Composable fun Dp.dpToPx() = with(LocalDensity.current) { this@dpToPx.toPx() } -@Composable -fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() } - -fun differenceBetweenMinutes( - minTime: LocalTime, - maxTime: LocalTime, -): Int { +fun differenceBetweenMinutes(minTime: LocalTime, maxTime: LocalTime): Int { return (maxTime.hour - minTime.hour) * 60 } fun differenceBetweenDays( minDate: LocalDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date, - maxDate: LocalDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date, + maxDate: LocalDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date ): Int { Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).time return (maxDate.dayOfMonth - minDate.dayOfMonth) } -fun differenceBetweenWeeks( - minDate: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()), - maxDate: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()), -): Int { - return (maxDate.dayOfMonth - minDate.dayOfMonth) +fun LocalDate.plusDays(days: Int): LocalDate { + return this.plus(days, DateTimeUnit.DAY) } -fun LocalDate.plusDays( - days: Int, -): LocalDate { - return this.plus(days, DateTimeUnit.DAY) +fun LocalDateTime.plusDays(days: Int): LocalDateTime { + return this.date.plus(days, DateTimeUnit.DAY).atTime(this.time) } -fun LocalTime.plusHours( - hours: Int, -): LocalTime { - val addedHours = this.hour + hours - return LocalTime(addedHours, this.minute) +fun LocalDate.minusDays(days: Int): LocalDate { + return this.minus(days, DateTimeUnit.DAY) } -fun LocalDateTime.plusWeeks( - weeks: Int, -): LocalDateTime { - return this.date.plusDays(weeks * 7).atStartOfDayIn( - TimeZone.currentSystemDefault() - ).toLocalDateTime( - TimeZone.currentSystemDefault() - ) +fun LocalDateTime.minusDays(days: Int): LocalDateTime { + return this.date.minus(days, DateTimeUnit.DAY).atTime(this.time) } -fun LocalDateTime.minusDays( - days: Int, -): LocalDateTime { - return this.date.minus(1, DateTimeUnit.DAY).atStartOfDayIn( - TimeZone.currentSystemDefault() - ).toLocalDateTime( - TimeZone.currentSystemDefault() - ) +fun LocalTime.plusHours(hours: Int): LocalTime { + val addedHours = this.hour + hours + return LocalTime(addedHours, this.minute) } fun LocalTime.truncatedTo(): LocalTime { return LocalTime(this.hour, this.minute) } -fun LocalTime.MIN(): LocalTime { +fun min(): LocalTime { return LocalTime(0, 0) } -fun LocalTime.MAX(): LocalTime { +fun max(): LocalTime { return LocalTime(23, 59, 59, 999999999) } -inline class SplitType private constructor(val value: Int) { +@JvmInline +value class SplitType private constructor(val value: Int) { companion object { val None = SplitType(0) val Start = SplitType(1) @@ -108,35 +110,40 @@ data class PositionedTask( val end: LocalTime, val col: Int = 0, val colSpan: Int = 1, - val colTotal: Int = 1, + val colTotal: Int = 1 ) -// val TaskTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("h:mm a") -// val DayFormatter = DateTimeFormatter.ofPattern("EE, MMM d") - sealed class ScheduleSize { class FixedSize(val size: Dp) : ScheduleSize() - class FixedCount(val count: Float) : ScheduleSize() { - constructor(count: Int) : this(count.toFloat()) - } + class FixedCount(val count: Float) : ScheduleSize() class Adaptive(val minSize: Dp) : ScheduleSize() } class TaskDataModifier( - val positionedTask: PositionedTask, + private val positionedTask: PositionedTask ) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?) = positionedTask } -fun Modifier.taskData(positionedTask: PositionedTask) = - this.then(TaskDataModifier(positionedTask)) +fun Modifier.taskData(positionedTask: PositionedTask) = this.then(TaskDataModifier(positionedTask)) -fun splitTasks(tasks: List): List { +fun splitTasks( + tasks: List, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int +): List { return tasks .map { task -> + val end = task.start.calculateEndTime( + focusSessions = task.focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) val startDate = task.start.date - val endDate = task.end.date + val endDate = end.date if (startDate == endDate) { listOf( PositionedTask( @@ -144,8 +151,8 @@ fun splitTasks(tasks: List): List { SplitType.None, task.start.date, task.start.time, - task.end.time, - ), + end.time + ) ) } else { val days = differenceBetweenDays(startDate, endDate) @@ -159,15 +166,13 @@ fun splitTasks(tasks: List): List { start = if (date == startDate) { task.start.time } else { - Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).time.MIN() + min() }, end = if (date == endDate) { - task.end.time + end.time } else { - Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).time.MAX() - }, + max() + } ) } splitTasks @@ -189,9 +194,9 @@ fun arrangeTasks(tasks: List): List { val groupTasks: MutableList> = mutableListOf() fun resetGroup() { - groupTasks.forEachIndexed { colIndex, col -> - col.forEach { e -> - positionedTasks.add(e.copy(col = colIndex, colTotal = groupTasks.size)) + groupTasks.forEachIndexed { columnIndex, column -> + column.forEach { e -> + positionedTasks.add(e.copy(col = columnIndex, colTotal = groupTasks.size)) } } groupTasks.clear() @@ -238,3 +243,378 @@ fun arrangeTasks(tasks: List): List { return positionedTasks } +fun Long?.selectedDateMillisToLocalDateTime(): LocalDateTime { + return Instant.fromEpochMilliseconds(this ?: 0) + .toLocalDateTime(TimeZone.currentSystemDefault()) +} + +fun calculateFromFocusSessions( + focusSessions: Int, + sessionTime: Int = 25, + shortBreakTime: Int = 5, + longBreakTime: Int = 15, + currentLocalDateTime: LocalDateTime +): LocalTime { + return if (focusSessions <= 0) { + currentLocalDateTime.time + } else { + val totalSessionTimeMinutes = sessionTime * focusSessions + val totalShortBreakTimeMinutes = shortBreakTime * (focusSessions - 1) + val totalLongBreakTimeMinutes = longBreakTime * (focusSessions / 4) + val totalBreakTimeMinutes = totalShortBreakTimeMinutes + totalLongBreakTimeMinutes + val totalTaskTimeMinutes = totalSessionTimeMinutes + totalBreakTimeMinutes + val totalTaskTimeMillis = totalTaskTimeMinutes.toEpochMilliseconds() + val totalTaskTimeLocalDateTime = + currentLocalDateTime.toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() + .plus(totalTaskTimeMillis).selectedDateMillisToLocalDateTime() + totalTaskTimeLocalDateTime.time + } +} + +fun Int.toEpochMilliseconds(): Long { + return this * 60 * 1000L +} + +fun LocalDateTime.dateTimeToString(): String { + return this.toString() +} + +fun toLocalDateTime(hour: Int, minute: Int, date: LocalDate): LocalDateTime { + return LocalDateTime( + date, + LocalTime(hour, minute) + ) +} + +fun String.isDigitsOnly(): Boolean { + return all { it.isDigit() } +} + +fun taskCompleteMessage(tasks: List): String { + val completedTasks = tasks.filter { it.completed }.size + return if (completedTasks == tasks.size && completedTasks != 0) { + "Congrats! You've completed all your tasks for today" + } else if (completedTasks == 0) { + "You've not completed any tasks for today" + } else if (taskCompletionPercentage(tasks) >= 90) { + "Keep it up! You're almost done with your daily tasks" + } else if (taskCompletionPercentage(tasks) >= 75) { + "Wow!, Your daily tasks are almost done" + } else if (taskCompletionPercentage(tasks) >= 50) { + "You're halfway through your daily tasks" + } else if (taskCompletionPercentage(tasks) >= 25) { + "You're almost halfway through your daily tasks" + } else if (taskCompletionPercentage(tasks) >= 10) { + "You've completed a few tasks for today" + } else { + "You've completed $completedTasks tasks for today" + } +} + +fun taskCompletionPercentage(tasks: List): Int { + val completedTasks = tasks.filter { it.completed }.size + return if (completedTasks == 0) { + 0 + } else { + (completedTasks.toFloat() / tasks.size.toFloat() * 100).toInt() + } +} + +fun String.taskColor(): Long { + return taskTypes.find { it.name == this }?.color ?: 0xFFAFBBF2 +} + +fun String.taskIcon(): String { + return taskTypes.find { it.name == this }?.icon ?: "other.xml" +} + +fun getThisWeek(): List { + /** + * From Monday to Sunday + */ + val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + val dayOfWeek = today.dayOfWeek.ordinal + val startOfWeek = today.minus(dayOfWeek, DateTimeUnit.DAY) + + /** + * Dates Between startOfWeek and endOfWeek inclusive + */ + val dates = mutableListOf() + for (i in 0..6) { + dates += startOfWeek.plus(i, DateTimeUnit.DAY) + } + return dates +} + +fun getPreviousWeek( + firstDateOfNextWeek: LocalDate = Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()).date +): List { + /** + * From Monday to Sunday + */ + val startOfWeek = firstDateOfNextWeek.minus(7, DateTimeUnit.DAY) + + /** + * Dates Between startOfWeek and endOfWeek inclusive + */ + val dates = mutableListOf() + for (i in 0..6) { + dates += startOfWeek.plus(i, DateTimeUnit.DAY) + } + return dates +} + +/** + * A function that will return the last 12 weeks + * The list should be in descending order + * The first item should be the current week - This week + * The second item should be the previous week - Last week + * + * The format of the result should be like > + * For this week -> > + * For the other weeks -> > + * If a week is outside of this year then it should be > + */ +fun getLast52Weeks(): List>> { + val thisYear = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).year + val thisWeek = getThisWeek() + val previousWeek = getPreviousWeek(thisWeek.first()) + val weeks = mutableListOf>>() + weeks += "This Week" to thisWeek + weeks += "Last Week" to previousWeek + for (i in 0..51) { + val week = getPreviousWeek(firstDateOfNextWeek = weeks.last().second.first()) + weeks += "${ + week.first().month.name.lowercase().capitalize(Locale.current).substring( + 0, + 3 + ) + } ${week.first().dayOfMonth} ${if (week.first().year != thisYear) week.first().year else ""}" + + " - ${ + week.last().month.name.lowercase().capitalize(Locale.current).substring( + 0, + 3 + ) + } ${week.last().dayOfMonth} ${if (week.last().year != thisYear) week.last().year else ""}" to week + } + return weeks +} + +fun List.completedTasks(dates: List): List { + return dates.map { date -> + filter { task -> + task.date.date == date + }.size + } +} + +fun List.aAllEntriesAreZero(): Boolean { + return all { it.toDouble() == 0.0 } +} + +fun LocalDate.prettyFormat(): String { + return "${this.dayOfMonth}${ + when (this.dayOfMonth) { + 1, 21, 31 -> "st" + 2, 22 -> "nd" + 3, 23 -> "rd" + else -> "th" + } + }, ${this.month.name.lowercase().capitalize(Locale.current).substring(0, 3)} ${this.year}" +} + +fun LocalDate.prettyPrintedMonthAndYear(): String { + return "${this.month.name.lowercase().capitalize(Locale.current).substring(0, 3)} ${this.year}" +} + +fun prettyTimeDifference(start: LocalDateTime, end: LocalDateTime, timeFormat: Int): String { + return if (timeFormat == 12) { + val startHourTo12HourSystem = if (start.hour > 12) { + start.hour - 12 + } else { + start.hour + } + val endHourTo12HourSystem = if (end.hour > 12) { + end.hour - 12 + } else { + end.hour + } + "$startHourTo12HourSystem:${start.minute.formattedZeroMinutes()} ${if (start.hour > 12) "PM" else "AM"} - ${ + endHourTo12HourSystem + }:${end.minute.formattedZeroMinutes()} ${if (end.hour > 12) "PM" else "AM"}" + } else { + "${start.hour}:${start.minute.formattedZeroMinutes()} - ${end.hour}:${end.minute.formattedZeroMinutes()}" + } +} + +fun Int.formattedZeroMinutes(): String { + return if (this < 10) { + "0$this" + } else { + this.toString() + } +} + +fun Long.formattedZeroMinutes(): String { + return if (this < 10) { + "0$this" + } else { + this.toString() + } +} + +fun LocalTime.formattedTimeBasedOnTimeFormat(timeFormat: Int): String { + return if (timeFormat == 12) { + val hourTo12HourSystem = if (this.hour > 12) { + this.hour - 12 + } else { + this.hour + } + "$hourTo12HourSystem:${ + this.minute.formattedZeroMinutes() + } ${if (this.hour > 12) "PM" else "AM"}" + } else { + "${this.hour}:${this.minute.formattedZeroMinutes()}" + } +} + +fun String.timeFormat(): Int { + return if (this == "12-hour") { + 12 + } else { + 24 + } +} + +fun Int.timeFormat(): String { + return if (this == 12) { + "12-hour" + } else { + "24-hour" + } +} + +fun Task.durationInMinutes( + focusSessions: Int, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int +): Int { + val end = start.calculateEndTime( + focusSessions = focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + return (end.time.toSecondOfDay() - start.time.toSecondOfDay()) / 60 +} + +/** + * LocalDates for List + * The last 1 year + * The next 1 year + */ +fun calendarLocalDates(): List { + val thisYear = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).year + val lastYear = thisYear - 1 + val nextYear = thisYear + 1 + val dates = mutableListOf() + for (i in 0..365) { + dates += LocalDate(thisYear, 1, 1).plus(i, DateTimeUnit.DAY) + } + for (i in 0..365) { + dates += LocalDate(lastYear, 1, 1).plus(i, DateTimeUnit.DAY) + } + for (i in 0..365) { + dates += LocalDate(nextYear, 1, 1).plus(i, DateTimeUnit.DAY) + } + return dates +} + +fun LocalDate.insideThisWeek(): Boolean { + val thisWeek = getThisWeek() + return this in thisWeek +} + +fun today(): LocalDateTime { + return Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()) +} + +fun Long.toTimer(): String { + /** + * Input is in milliseconds + */ + val seconds = this / 1000 + val minutes = seconds / 60 + val hours = minutes / 60 + return "${ + if (hours > 0) { + hours.formattedZeroMinutes() + ":" + } else { + "" + } + }${(minutes - (hours * 60)).formattedZeroMinutes()}:${(seconds - (minutes * 60)).formattedZeroMinutes()}" +} + +fun Long.toPercentage(total: Long): Float { + /** + * In increase order + */ + return if (total == 0L) { + 0F + } else { + val perc = (100 - ((this.toFloat() / total.toFloat()) * 100)) + perc + } +} + +fun Long.toMinutes(): Int { + return (this / 1000 / 60).toInt() +} + +fun Int.toMillis(): Long { + /** + * Input is minutes + */ + return (this * 60 * 1000).toLong() +} + +fun String?.sessionType(): SessionType { + return when (this) { + "Focus" -> SessionType.Focus + "ShortBreak" -> SessionType.ShortBreak + "LongBreak" -> SessionType.LongBreak + else -> SessionType.Focus + } +} + +val LocalAppNavigator: ProvidableCompositionLocal = staticCompositionLocalOf { null } + +@Composable +fun ProvideAppNavigator(navigator: Navigator, content: @Composable () -> Unit) { + CompositionLocalProvider(LocalAppNavigator provides navigator) { + content() + } +} + +fun String.pickFirstName(): String { + return this.split(" ").first() +} + +fun LocalDateTime.calculateEndTime( + focusSessions: Int, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int +): LocalDateTime { + val totalSessionTimeMinutes = sessionTime * focusSessions + val totalShortBreakTimeMinutes = shortBreakTime * (focusSessions - 1) + val totalLongBreakTimeMinutes = longBreakTime * (focusSessions / 4) + val totalBreakTimeMinutes = totalShortBreakTimeMinutes + totalLongBreakTimeMinutes + val totalTaskTimeMinutes = totalSessionTimeMinutes + totalBreakTimeMinutes + val totalTaskTimeMillis = totalTaskTimeMinutes.toEpochMilliseconds() + return toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() + .plus(totalTaskTimeMillis).selectedDateMillisToLocalDateTime() +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/CommonModule.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/CommonModule.kt index 96969dd..b6ba63e 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/CommonModule.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/CommonModule.kt @@ -1,8 +1,122 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.di +import com.joelkanyi.focusbloom.core.data.local.setting.PreferenceManager +import com.joelkanyi.focusbloom.core.data.repository.settings.SettingsRepositoryImpl +import com.joelkanyi.focusbloom.core.data.repository.tasks.TasksRepositoryImpl +import com.joelkanyi.focusbloom.core.domain.repository.settings.SettingsRepository +import com.joelkanyi.focusbloom.core.domain.repository.tasks.TasksRepository +import com.joelkanyi.focusbloom.database.BloomDatabase +import com.joelkanyi.focusbloom.feature.addtask.AddTaskScreenModel +import com.joelkanyi.focusbloom.feature.calendar.CalendarScreenModel +import com.joelkanyi.focusbloom.feature.home.HomeScreenModel +import com.joelkanyi.focusbloom.feature.onboarding.OnboadingViewModel +import com.joelkanyi.focusbloom.feature.settings.SettingsScreenModel +import com.joelkanyi.focusbloom.feature.statistics.StatisticsScreenModel +import com.joelkanyi.focusbloom.feature.taskprogress.TaskProgressScreenModel +import com.joelkanyi.focusbloom.main.MainViewModel +import com.joelkanyi.focusbloom.platform.DatabaseDriverFactory +import com.russhwolf.settings.ExperimentalSettingsApi +import org.koin.core.module.Module import org.koin.dsl.module -fun commonModule(isDebug: Boolean) = module { +@OptIn(ExperimentalSettingsApi::class) +fun commonModule() = module { + /** + * Database + */ + single { + BloomDatabase( + driver = get().createDriver() + ) + } + /** + * Multiplatform-Settings + */ + single { + PreferenceManager(observableSettings = get()) + } + + /** + * Repositories + */ + single { + SettingsRepositoryImpl( + preferenceManager = get() + ) + } + + single { + TasksRepositoryImpl( + bloomDatabase = get() + ) + } + + /** + * ViewModels + */ + single { + SettingsScreenModel( + settingsRepository = get() + ) + } + single { + AddTaskScreenModel( + settingsRepository = get(), + tasksRepository = get() + ) + } + single { + HomeScreenModel( + tasksRepository = get(), + settingsRepository = get() + ) + } + single { + StatisticsScreenModel( + tasksRepository = get(), + settingsRepository = get() + ) + } + single { + CalendarScreenModel( + tasksRepository = get(), + settingsRepository = get() + ) + } + + single { + TaskProgressScreenModel( + settingsRepository = get(), + tasksRepository = get() + ) + } + + single { + MainViewModel( + settingsRepository = get() + ) + } + + single { + OnboadingViewModel( + settingsRepository = get() + ) + } } -// expect fun platformModule(): Module +expect fun platformModule(): Module diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/DIAccessor.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/DIAccessor.kt deleted file mode 100644 index c9e320b..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/DIAccessor.kt +++ /dev/null @@ -1 +0,0 @@ -package com.joelkanyi.focusbloom.di diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/InitKoin.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/InitKoin.kt deleted file mode 100644 index 78a1985..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/InitKoin.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.joelkanyi.focusbloom.di - -import org.koin.core.Koin -import org.koin.core.context.startKoin - -class InitKoin { - operator fun invoke(): Koin { - return startKoin { - modules( - listOf( - commonModule(true), - ), - ) - }.koin - } -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/Koin.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/Koin.kt deleted file mode 100644 index 4790987..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/Koin.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.joelkanyi.focusbloom.di - -import org.koin.core.context.startKoin -import org.koin.dsl.KoinAppDeclaration - -fun initKoin(appDeclaration: KoinAppDeclaration = {}) = - startKoin { - appDeclaration() - } diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/KoinInit.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/KoinInit.kt new file mode 100644 index 0000000..5a484ab --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/di/KoinInit.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.di + +import org.koin.core.Koin +import org.koin.core.context.startKoin +import org.koin.dsl.KoinAppDeclaration + +class KoinInit { + fun init(appDeclaration: KoinAppDeclaration = {}): Koin { + return startKoin { + modules( + listOf( + platformModule(), + commonModule() + ) + ) + appDeclaration() + }.koin + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/domain/model/Task.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/domain/model/Task.kt deleted file mode 100644 index 02a9595..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/domain/model/Task.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.joelkanyi.focusbloom.domain.model - -import kotlinx.datetime.LocalDateTime - -data class Task( - val id: Int = 0, - val name: String, - val description: String? = null, - val start: LocalDateTime, - val end: LocalDateTime, - val color: Long, - val current: Int, - val date: LocalDateTime, -) diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/domain/model/TextFieldState.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/domain/model/TextFieldState.kt deleted file mode 100644 index 2d6d474..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/domain/model/TextFieldState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.joelkanyi.focusbloom.domain.model - -data class TextFieldState( - val text: String = "", - val error: String? = null, -) diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/addtask/AddTaskScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/addtask/AddTaskScreen.kt new file mode 100644 index 0000000..173f0d1 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/addtask/AddTaskScreen.kt @@ -0,0 +1,667 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.addtask + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.DatePickerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TimeInput +import androidx.compose.material3.TimePickerState +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberTimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogProperties +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.domain.model.TaskType +import com.joelkanyi.focusbloom.core.domain.model.TextFieldState +import com.joelkanyi.focusbloom.core.domain.model.taskTypes +import com.joelkanyi.focusbloom.core.presentation.component.BloomButton +import com.joelkanyi.focusbloom.core.presentation.component.BloomDateBoxField +import com.joelkanyi.focusbloom.core.presentation.component.BloomDropDown +import com.joelkanyi.focusbloom.core.presentation.component.BloomIncrementer +import com.joelkanyi.focusbloom.core.presentation.component.BloomInputTextField +import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar +import com.joelkanyi.focusbloom.core.presentation.theme.SuccessColor +import com.joelkanyi.focusbloom.core.utils.UiEvents +import com.joelkanyi.focusbloom.core.utils.calculateFromFocusSessions +import com.joelkanyi.focusbloom.core.utils.formattedTimeBasedOnTimeFormat +import com.joelkanyi.focusbloom.core.utils.selectedDateMillisToLocalDateTime +import com.joelkanyi.focusbloom.core.utils.toLocalDateTime +import com.joelkanyi.focusbloom.core.utils.today +import com.joelkanyi.focusbloom.platform.StatusBarColors +import kotlinx.coroutines.flow.collectLatest +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource +import org.koin.compose.rememberKoinInject + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@Composable +fun AddTaskScreen() { + val screenModel: AddTaskScreenModel = rememberKoinInject() + + StatusBarColors( + statusBarColor = MaterialTheme.colorScheme.background, + navBarColor = MaterialTheme.colorScheme.background + ) + val snackbarHostState = remember { SnackbarHostState() } + val keyboardController = LocalSoftwareKeyboardController.current + val navigator = LocalNavigator.currentOrThrow + val sessionTime = screenModel.sessionTime.collectAsState().value ?: 25 + val shortBreakTime = screenModel.shortBreakTime.collectAsState().value ?: 5 + val longBreakTime = screenModel.longBreakTime.collectAsState().value ?: 15 + val hourFormat = screenModel.hourFormat.collectAsState().value ?: 24 + val focusSessions = screenModel.focusSessions.collectAsState().value + val showStartTimeInputDialog = screenModel.showStartTimeInputDialog.collectAsState().value + val showTaskDatePickerDialog = screenModel.showTaskDatePickerDialog.collectAsState().value + val selectedTaskType = screenModel.selectedOption.collectAsState().value + val taskName = screenModel.taskName.value + val taskDescription = screenModel.taskDescription.value + + val startTimeState = rememberTimePickerState( + initialHour = today().hour, + initialMinute = today().minute, + is24Hour = hourFormat == 24 + ) + val datePickerState = rememberDatePickerState( + initialSelectedDateMillis = Clock.System.now().toEpochMilliseconds() + ) + val calculatedFocusTime by remember { + mutableStateOf( + calculateFromFocusSessions( + focusSessions = focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime, + currentLocalDateTime = LocalDateTime( + year = datePickerState.selectedDateMillis.selectedDateMillisToLocalDateTime().year, + month = datePickerState.selectedDateMillis.selectedDateMillisToLocalDateTime().month, + dayOfMonth = datePickerState.selectedDateMillis.selectedDateMillisToLocalDateTime().dayOfMonth, + hour = startTimeState.hour, + minute = startTimeState.minute + ) + ) + ) + } + + LaunchedEffect(key1 = true) { + screenModel.eventsFlow.collectLatest { event -> + when (event) { + is UiEvents.ShowSnackbar -> { + snackbarHostState.showSnackbar( + message = event.message + ) + datePickerState.setSelection( + Clock.System.now().toEpochMilliseconds() + ) + } + + UiEvents.NavigateBack -> { + navigator.pop() + } + + else -> {} + } + } + } + + if (showStartTimeInputDialog) { + TimerInputDialog( + title = "Start Time", + state = startTimeState, + onDismiss = { + screenModel.setShowStartTimeInputDialog(false) + } + ) + } + + if (showTaskDatePickerDialog) { + TaskDatePicker( + datePickerState = datePickerState, + dismiss = { + screenModel.setShowTaskDatePickerDialog(false) + } + ) + } + + AddTaskScreenContent( + snackbarHostState = snackbarHostState, + hourFormat = hourFormat, + calculatedFocusTime = calculatedFocusTime, + taskOptions = taskTypes, + selectedTaskType = selectedTaskType, + taskName = taskName, + taskDescription = taskDescription, + datePickerState = datePickerState, + focusSessions = focusSessions, + startTimePickerState = startTimeState, + onTaskNameChange = { + screenModel.setTaskName(it) + }, + onIncrementFocusSessions = { + screenModel.incrementFocusSessions() + }, + onDecrementIncrementFocusSessions = { + screenModel.decrementFocusSessions() + }, + onSelectedTaskTypeChange = { + screenModel.setSelectedOption(it) + }, + onTaskDescriptionChange = { + screenModel.setTaskDescription(it) + }, + onClickPickStartTime = { + screenModel.setShowStartTimeInputDialog(true) + }, + onClickPickDate = { + screenModel.setShowTaskDatePickerDialog(true) + }, + onClickAddTask = { + keyboardController?.hide() + screenModel.addTask( + task = Task( + name = taskName, + description = taskDescription, + start = toLocalDateTime( + date = datePickerState.selectedDateMillis.selectedDateMillisToLocalDateTime().date, + hour = startTimeState.hour, + minute = startTimeState.minute + ), + /*end = toLocalDateTime( + date = datePickerState.selectedDateMillis.selectedDateMillisToLocalDateTime().date, + hour = calculatedFocusTime.hour, + minute = calculatedFocusTime.minute, + ),*/ + color = selectedTaskType.color, + current = "Focus", + date = datePickerState.selectedDateMillis.selectedDateMillisToLocalDateTime(), + focusSessions = focusSessions, + completed = false, + /*focusTime = sessionTime.toMillis(), + shortBreakTime = shortBreakTime.toMillis(), + longBreakTime = longBreakTime.toMillis(),*/ + type = selectedTaskType.name, + consumedFocusTime = 0L, + consumedShortBreakTime = 0L, + consumedLongBreakTime = 0L, + inProgressTask = false, + currentCycle = 0, + active = false + ) + ) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalResourceApi::class) +@Composable +private fun AddTaskScreenContent( + snackbarHostState: SnackbarHostState, + calculatedFocusTime: LocalTime, + hourFormat: Int, + taskOptions: List, + selectedTaskType: TaskType, + onSelectedTaskTypeChange: (TaskType) -> Unit, + taskName: String, + taskDescription: String, + onTaskDescriptionChange: (String) -> Unit, + focusSessions: Int, + onTaskNameChange: (String) -> Unit, + onIncrementFocusSessions: () -> Unit, + onDecrementIncrementFocusSessions: () -> Unit, + onClickAddTask: () -> Unit, + onClickPickStartTime: () -> Unit, + onClickPickDate: () -> Unit, + startTimePickerState: TimePickerState, + datePickerState: DatePickerState +) { + Scaffold( + snackbarHost = { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter // Change to your desired position + ) { + SnackbarHost( + hostState = snackbarHostState, + snackbar = { + Card( + modifier = Modifier + .padding(horizontal = 16.dp) + .clickable { + snackbarHostState.currentSnackbarData?.dismiss() + }, + border = BorderStroke(2.dp, MaterialTheme.colorScheme.primary), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondary + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier + .fillMaxWidth(.85f), + text = it.visuals.message, + style = MaterialTheme.typography.titleSmall.copy( + color = MaterialTheme.colorScheme.onPrimary + ) + ) + Image( + modifier = Modifier + .size(32.dp), + painter = painterResource("ic_complete.xml"), + contentDescription = "Task Options" + ) + } + } + } + ) + } + }, + topBar = { + BloomTopAppBar { + Text(text = "Add Task") + } + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier.padding(paddingValues), + contentPadding = PaddingValues(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + item { + BloomInputTextField( + modifier = Modifier.fillMaxWidth(), + maxLines = 3, + label = { + Text( + text = "Task Name", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp + ) + ) + }, + value = TextFieldState(text = taskName), + onValueChange = onTaskNameChange, + placeholder = { + Text( + text = "Enter Task Name", + style = MaterialTheme.typography.titleSmall + + ) + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Words + ), + textStyle = MaterialTheme.typography.titleSmall.copy( + fontSize = 16.sp + ) + ) + } + item { + BloomInputTextField( + modifier = Modifier.fillMaxWidth(), + maxLines = 5, + label = { + Text( + text = "Description", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp + ) + + ) + }, + value = TextFieldState(text = taskDescription), + onValueChange = onTaskDescriptionChange, + placeholder = { + Text( + text = "Enter Description", + style = MaterialTheme.typography.titleSmall + ) + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences + ), + textStyle = MaterialTheme.typography.titleSmall.copy( + fontSize = 16.sp + ) + ) + } + item { + BloomDateBoxField( + modifier = Modifier + .fillMaxWidth(), + label = { + Text( + text = "Date", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp + ) + ) + }, + currentTextState = TextFieldState( + text = datePickerState.selectedDateMillis.selectedDateMillisToLocalDateTime().date.toString() + ), + onClick = onClickPickDate, + textStyle = MaterialTheme.typography.titleSmall.copy( + fontSize = 16.sp + ) + ) + } + + item { + BloomDropDown( + label = { + Text( + text = "Task Type", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp + ) + ) + }, + modifier = Modifier.fillMaxWidth(), + options = taskOptions, + selectedOption = TextFieldState(selectedTaskType.name), + onOptionSelected = onSelectedTaskTypeChange, + textStyle = MaterialTheme.typography.titleSmall.copy( + fontSize = 16.sp + ) + ) + } + + item { + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + TimeComponent( + time = LocalTime( + startTimePickerState.hour, + startTimePickerState.minute + ), + hourFormat = hourFormat, + title = "Start Time", + icon = "start_time.xml", + iconColor = MaterialTheme.colorScheme.primary, + iconSize = 24, + onClick = onClickPickStartTime + ) + + DashedDivider( + color = MaterialTheme.colorScheme.primary, + thickness = 3.dp, + phase = 5f, + modifier = Modifier + .width(180.dp) + ) + + TimeComponent( + time = calculatedFocusTime, + hourFormat = hourFormat, + title = "End Time", + icon = "end_time.xml", + iconColor = SuccessColor, + onClick = {} + ) + } + } + + item { + Text( + modifier = Modifier.fillMaxWidth(), + text = "Focus Sessions", + style = MaterialTheme.typography.titleMedium.copy( + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp + ) + ) + } + + item { + BloomIncrementer( + modifier = Modifier.fillMaxWidth(), + onClickRemove = { + onDecrementIncrementFocusSessions() + }, + onClickAdd = { + onIncrementFocusSessions() + }, + currentValue = focusSessions + ) + } + + item { + Spacer(modifier = Modifier.height(12.dp)) + } + + item { + BloomButton( + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + onClick = onClickAddTask, + content = { + Text(text = "Save") + } + ) + } + } + } +} + +@OptIn(ExperimentalResourceApi::class) +@Composable +private fun TimeComponent( + title: String, + icon: String, + iconColor: Color, + iconSize: Int = 32, + time: LocalTime, + hourFormat: Int, + onClick: () -> Unit +) { + Column( + modifier = Modifier.clickable { + onClick() + }, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp + ) + ) + Row( + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = CenterVertically + ) { + Text( + text = time.formattedTimeBasedOnTimeFormat(hourFormat), + style = MaterialTheme.typography.titleSmall.copy( + fontSize = 16.sp + ) + ) + + Icon( + modifier = Modifier + .size(iconSize.dp), + painter = painterResource(icon), + contentDescription = title, + tint = iconColor + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TimerInputDialog( + title: String, + modifier: Modifier = Modifier, + onDismiss: () -> Unit, + state: TimePickerState +) { + AlertDialog( + properties = DialogProperties(usePlatformDefaultWidth = true), + modifier = modifier, + onDismissRequest = onDismiss, + title = { + Text( + text = title, + style = MaterialTheme.typography.titleMedium + ) + }, + text = { + TimeInput( + modifier = Modifier.fillMaxWidth(), + state = state + ) + }, + dismissButton = { + TextButton( + onClick = onDismiss, + content = { + Text(text = "Cancel") + } + ) + }, + confirmButton = { + TextButton( + onClick = onDismiss, + content = { + Text(text = "OK") + } + ) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TaskDatePicker(datePickerState: DatePickerState, dismiss: () -> Unit) { + DatePickerDialog( + onDismissRequest = { dismiss() }, + dismissButton = { + TextButton(onClick = dismiss) { + Text(text = "Cancel") + } + }, + confirmButton = { + TextButton( + onClick = { + datePickerState + .selectedDateMillis + dismiss() + } + ) { + Text(text = "OK") + } + } + ) { + DatePicker(state = datePickerState) + } +} + +@Composable +fun DashedDivider( + thickness: Dp, + color: Color = MaterialTheme.colorScheme.onSurfaceVariant, + phase: Float = 10f, + intervals: FloatArray = floatArrayOf(10f, 10f), + modifier: Modifier +) { + Canvas( + modifier = modifier + ) { + val dividerHeight = thickness.toPx() + drawRoundRect( + color = color, + style = Stroke( + width = dividerHeight, + pathEffect = PathEffect.dashPathEffect( + intervals = intervals, + phase = phase + ) + ) + ) + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/addtask/AddTaskScreenModel.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/addtask/AddTaskScreenModel.kt new file mode 100644 index 0000000..7e40172 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/addtask/AddTaskScreenModel.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.addtask + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.domain.model.TaskType +import com.joelkanyi.focusbloom.core.domain.model.taskTypes +import com.joelkanyi.focusbloom.core.domain.repository.settings.SettingsRepository +import com.joelkanyi.focusbloom.core.domain.repository.tasks.TasksRepository +import com.joelkanyi.focusbloom.core.utils.UiEvents +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class AddTaskScreenModel( + settingsRepository: SettingsRepository, + private val tasksRepository: TasksRepository +) : ScreenModel { + private val _eventsFlow = MutableSharedFlow() + val eventsFlow = _eventsFlow.asSharedFlow() + + val sessionTime = settingsRepository.getSessionTime() + .map { + it + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + val shortBreakTime = settingsRepository.getShortBreakTime() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + val longBreakTime = settingsRepository.getLongBreakTime() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + val hourFormat = settingsRepository.getHourFormat() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + private val _focusSessions = MutableStateFlow(0) + val focusSessions = _focusSessions.asStateFlow() + fun incrementFocusSessions() { + _focusSessions.value++ + } + + fun decrementFocusSessions() { + if (_focusSessions.value > 0) { + _focusSessions.value-- + } + } + + private val _taskName = mutableStateOf("") + val taskName: State = _taskName + fun setTaskName(name: String) { + _taskName.value = name + } + + private val _taskDescription = mutableStateOf("") + val taskDescription: State = _taskDescription + fun setTaskDescription(description: String) { + _taskDescription.value = description + } + + private val _selectedOption = MutableStateFlow(taskTypes.last()) + val selectedOption = _selectedOption.asStateFlow() + fun setSelectedOption(option: TaskType) { + _selectedOption.value = option + } + + private val _showStartTimeInputDialog = MutableStateFlow(false) + val showStartTimeInputDialog = _showStartTimeInputDialog.asStateFlow() + fun setShowStartTimeInputDialog(show: Boolean) { + _showStartTimeInputDialog.value = show + } + + private val _showTaskDatePickerDialog = MutableStateFlow(false) + val showTaskDatePickerDialog = _showTaskDatePickerDialog.asStateFlow() + fun setShowTaskDatePickerDialog(show: Boolean) { + _showTaskDatePickerDialog.value = show + } + + fun addTask(task: Task) { + coroutineScope.launch { + tasksRepository.addTask(task) + _focusSessions.value = 0 + _taskName.value = "" + _taskDescription.value = "" + _selectedOption.value = taskTypes.last() + _showStartTimeInputDialog.value = false + _eventsFlow.emit(UiEvents.ShowSnackbar("Task added!")) + } + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/calendar/CalendarScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/calendar/CalendarScreen.kt new file mode 100644 index 0000000..64968fd --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/calendar/CalendarScreen.kt @@ -0,0 +1,736 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.calendar + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.times +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar +import com.joelkanyi.focusbloom.core.utils.PositionedTask +import com.joelkanyi.focusbloom.core.utils.ScheduleSize +import com.joelkanyi.focusbloom.core.utils.SplitType +import com.joelkanyi.focusbloom.core.utils.arrangeTasks +import com.joelkanyi.focusbloom.core.utils.calculateEndTime +import com.joelkanyi.focusbloom.core.utils.calendarLocalDates +import com.joelkanyi.focusbloom.core.utils.differenceBetweenDays +import com.joelkanyi.focusbloom.core.utils.differenceBetweenMinutes +import com.joelkanyi.focusbloom.core.utils.dpToPx +import com.joelkanyi.focusbloom.core.utils.durationInMinutes +import com.joelkanyi.focusbloom.core.utils.formattedTimeBasedOnTimeFormat +import com.joelkanyi.focusbloom.core.utils.insideThisWeek +import com.joelkanyi.focusbloom.core.utils.max +import com.joelkanyi.focusbloom.core.utils.min +import com.joelkanyi.focusbloom.core.utils.plusHours +import com.joelkanyi.focusbloom.core.utils.prettyPrintedMonthAndYear +import com.joelkanyi.focusbloom.core.utils.prettyTimeDifference +import com.joelkanyi.focusbloom.core.utils.splitTasks +import com.joelkanyi.focusbloom.core.utils.taskColor +import com.joelkanyi.focusbloom.core.utils.taskData +import com.joelkanyi.focusbloom.core.utils.today +import com.joelkanyi.focusbloom.core.utils.truncatedTo +import com.joelkanyi.focusbloom.platform.StatusBarColors +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource +import org.koin.compose.rememberKoinInject +import kotlin.math.roundToInt + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@Composable +fun CalendarScreen() { + val screenModel: CalendarScreenModel = rememberKoinInject() + + StatusBarColors( + statusBarColor = MaterialTheme.colorScheme.background, + navBarColor = MaterialTheme.colorScheme.background + ) + val coroutineScope = rememberCoroutineScope() + val tasks = screenModel.tasks.collectAsState().value + val selectedDay = screenModel.selectedDay.collectAsState().value + val hourFormat = screenModel.hourFormat.collectAsState().value ?: 24 + val calendarPagerState = rememberLazyListState() + val verticalScrollState = rememberScrollState() + val sessionTime = screenModel.sessionTime.collectAsState().value ?: 25 + val shortBreakTime = screenModel.shortBreakTime.collectAsState().value ?: 5 + val longBreakTime = screenModel.longBreakTime.collectAsState().value ?: 15 + LaunchedEffect(key1 = tasks, block = { + calendarPagerState.animateScrollToItem( + index = calendarLocalDates().indexOf(selectedDay), + scrollOffset = 0 + ) + }) + BoxWithConstraints { + val windowSizeClass = calculateWindowSizeClass() + val useDesktopSize = windowSizeClass.widthSizeClass > WindowWidthSizeClass.Compact + val hourSize = if (useDesktopSize) 90.dp else 92.dp + val daySize = if (useDesktopSize) this.maxWidth - 72.dp else this.maxWidth - 72.dp + CalendarScreenContent( + selectedDay = selectedDay, + hourFormat = hourFormat, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime, + hourSize = ScheduleSize.FixedSize(hourSize), + daySize = ScheduleSize.FixedSize(daySize), + selectedDayTasks = tasks.filter { + it.date.date == selectedDay + }, + verticalScrollState = verticalScrollState, + calendarPagerState = calendarPagerState, + onClickThisWeek = { + coroutineScope.launch { + calendarPagerState.animateScrollToItem( + index = calendarLocalDates().indexOf( + Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()).date + ), + scrollOffset = 0 + ) + screenModel.setSelectedDay( + Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()).date + ) + } + }, + onSelectDay = { + screenModel.setSelectedDay(it) + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalResourceApi::class) +@Composable +fun CalendarScreenContent( + verticalScrollState: ScrollState, + calendarPagerState: LazyListState, + hourSize: ScheduleSize, + daySize: ScheduleSize, + hourFormat: Int, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int, + selectedDayTasks: List, + selectedDay: LocalDate, + onClickThisWeek: () -> Unit, + onSelectDay: (LocalDate) -> Unit +) { + Scaffold( + topBar = { + BloomTopAppBar( + hasBackNavigation = false, + title = { + Text(text = "Calendar") + }, + actions = { + AnimatedVisibility(selectedDay.insideThisWeek().not()) { + TextButton( + onClick = onClickThisWeek + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + modifier = Modifier.size(18.dp), + painter = painterResource("redo.xml"), + contentDescription = "Today" + ) + Text( + text = "TODAY", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline + ) + ) + } + } + } + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .padding(PaddingValues(horizontal = 16.dp)), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = selectedDay.prettyPrintedMonthAndYear(), + style = MaterialTheme.typography.labelLarge.copy( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.End + ) + ) + LazyRow( + state = calendarPagerState, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(calendarLocalDates()) { date -> + Box( + modifier = Modifier + .size(64.dp) + .clipToBounds() + .background( + color = if (date == selectedDay) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(4.dp) + ) + .clickable { + onSelectDay(date) + }, + contentAlignment = Alignment.Center + ) { + Column { + Text( + text = date.dayOfWeek.name.substring(0, 3), + style = MaterialTheme.typography.labelMedium.copy( + color = if (date == selectedDay) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface + ), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Text( + text = date.dayOfMonth.toString(), + style = MaterialTheme.typography.labelMedium.copy( + color = if (date == selectedDay) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface + ), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + } + } + } + Box(modifier = Modifier.fillMaxSize()) { + if (selectedDayTasks.isNotEmpty()) { + Schedule( + verticalScrollState = verticalScrollState, + hourFormat = hourFormat, + tasks = selectedDayTasks.sortedBy { it.start }, + hourSize = hourSize, + daySize = daySize, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + } else { + Text( + text = "No tasks for ${ + if (selectedDay == today().date) { + "today" + } else { + "this day" + } + }", + style = MaterialTheme.typography.labelLarge, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.Center) + ) + } + } + } + } +} + +@Composable +fun BasicTask( + hourFormat: Int, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int, + positionedTask: PositionedTask, + modifier: Modifier = Modifier +) { + val task = positionedTask.task + val end by remember { + mutableStateOf( + task.start.calculateEndTime( + focusSessions = task.focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + ) + } + val topRadius = + if (positionedTask.splitType == SplitType.Start || positionedTask.splitType == SplitType.Both) 0.dp else 2.dp + val bottomRadius = + if (positionedTask.splitType == SplitType.End || positionedTask.splitType == SplitType.Both) 0.dp else 2.dp + Card( + modifier = modifier + .fillMaxSize() + .padding( + top = 2.dp, + end = 2.dp, + bottom = if (positionedTask.splitType == SplitType.End) 0.dp else 2.dp + ) + .clipToBounds() + .padding(4.dp), + shape = RoundedCornerShape( + topStart = topRadius, + topEnd = topRadius, + bottomEnd = bottomRadius, + bottomStart = bottomRadius + ), + colors = CardDefaults.cardColors( + containerColor = Color(task.type.taskColor()) + ) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = task.name, + style = MaterialTheme.typography.labelMedium.copy( + color = MaterialTheme.colorScheme.onPrimary, + fontSize = 14.sp + ), + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (task.description != null) { + Text( + text = task.description, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onPrimary, + fontSize = 12.sp + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "${ + task.durationInMinutes( + focusSessions = task.focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + } minutes", + style = MaterialTheme.typography.displaySmall.copy( + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.SemiBold, + fontSize = 10.sp + ) + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = prettyTimeDifference( + start = task.start, + end = end, + timeFormat = hourFormat + ), + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.SemiBold, + fontSize = 10.sp + ), + maxLines = 1, + overflow = TextOverflow.Clip, + textAlign = TextAlign.End + ) + } + } + } +} + +@Composable +fun BasicSidebarLabel(hourFormat: Int, time: LocalTime, modifier: Modifier = Modifier) { + Text( + text = time.formattedTimeBasedOnTimeFormat(hourFormat), + modifier = modifier + .fillMaxHeight() + .padding(4.dp), + style = MaterialTheme.typography.labelMedium.copy( + fontSize = 12.sp + ) + ) +} + +@Composable +fun ScheduleSidebar( + hourFormat: Int, + hourHeight: Dp, + modifier: Modifier = Modifier, + minTime: LocalTime = min(), + maxTime: LocalTime = max(), + label: @Composable (hourFormat: Int, time: LocalTime) -> Unit = { _, time -> + BasicSidebarLabel( + hourFormat = hourFormat, + time = time + ) + } +) { + val numMinutes = differenceBetweenMinutes(minTime, maxTime) + 1 + val numHours = numMinutes / 60 + val firstHour = minTime.truncatedTo() + val firstHourOffsetMinutes = + if (firstHour == minTime) 0 else differenceBetweenMinutes(minTime, firstHour.plusHours(1)) + val firstHourOffset = hourHeight * (firstHourOffsetMinutes / 60f) + val startTime = if (firstHour == minTime) firstHour else firstHour.plusHours(1) + Column(modifier = modifier) { + Spacer(modifier = Modifier.height(firstHourOffset)) + repeat(numHours) { i -> + Box(modifier = Modifier.height(hourHeight)) { + label( + hourFormat, + startTime.plusHours(i) + ) + } + } + } +} + +@Composable +fun Schedule( + hourFormat: Int, + tasks: List, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int, + verticalScrollState: ScrollState, + modifier: Modifier = Modifier, + taskContent: @Composable ( + positionedTask: PositionedTask + ) -> Unit = { positionedTask -> + BasicTask( + hourFormat = hourFormat, + positionedTask = positionedTask, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + }, + timeLabel: @Composable (hourFormat: Int, time: LocalTime) -> Unit = { hrFormat, time -> + BasicSidebarLabel( + hourFormat = hrFormat, + time = time + ) + }, + minDate: LocalDate = tasks.minByOrNull(Task::start)?.start?.date ?: Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()).date, + // maxDate: LocalDate = tasks.maxByOrNull(Task::end)?.end?.date ?: Clock.System.now() + // .toLocalDateTime(TimeZone.currentSystemDefault()).date, + maxDate: LocalDate = tasks.map { + it.start.calculateEndTime( + focusSessions = it.focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + }.maxOfOrNull { it.date } ?: today().date, + minTime: LocalTime = min(), + maxTime: LocalTime = max(), + daySize: ScheduleSize, + hourSize: ScheduleSize +) { + val numDays = 0 + 1 + val numMinutes = differenceBetweenMinutes(minTime, maxTime) + 1 + val numHours = numMinutes.toFloat() / 60f + val scope = rememberCoroutineScope() + + /** + * Scroll to the closest task + * */ + // LaunchedEffect(key1 = tasks, block = { + when (hourSize) { + is ScheduleSize.Adaptive -> { + val task = tasks.minByOrNull { it.start } + if (task != null) { + val taskStartMinutes = differenceBetweenMinutes(minTime, task.start.time) + val taskStartHours = taskStartMinutes / 60f + val taskStartOffset = taskStartHours * hourSize.minSize.dpToPx() + LaunchedEffect(key1 = tasks, block = { + scope.launch { + verticalScrollState.animateScrollTo(taskStartOffset.roundToInt()) + } + }) + } + } + + is ScheduleSize.FixedCount -> { + val task = tasks.minByOrNull { it.start } + if (task != null) { + val taskStartMinutes = differenceBetweenMinutes(minTime, task.start.time) + val taskStartHours = taskStartMinutes / 60f + val taskStartOffset = taskStartHours * hourSize.count.dp + LaunchedEffect(key1 = tasks, block = { + scope.launch { + verticalScrollState.animateScrollTo(taskStartOffset.value.roundToInt()) + } + }) + } + } + + is ScheduleSize.FixedSize -> { + // Scroll to the closest task + val task = tasks.minByOrNull { it.start } + if (task != null) { + val taskStartMinutes = differenceBetweenMinutes(minTime, task.start.time) + val taskStartHours = taskStartMinutes / 60f + val taskStartOffset = taskStartHours * hourSize.size.dpToPx() + LaunchedEffect(key1 = tasks, block = { + scope.launch { + verticalScrollState.animateScrollTo(taskStartOffset.roundToInt()) + } + }) + } + } + } + // }) + + var sidebarWidth by remember { mutableStateOf(0) } + BoxWithConstraints(modifier = modifier) { + val dayWidth: Dp = when (daySize) { + is ScheduleSize.FixedSize -> daySize.size + is ScheduleSize.FixedCount -> with(LocalDensity.current) { ((constraints.maxWidth - sidebarWidth) / daySize.count).toDp() } + is ScheduleSize.Adaptive -> with(LocalDensity.current) { + maxOf( + ((constraints.maxWidth - sidebarWidth) / numDays).toDp(), + daySize.minSize + ) + } + } + val hourHeight: Dp = when (hourSize) { + is ScheduleSize.FixedSize -> hourSize.size + is ScheduleSize.FixedCount -> with(LocalDensity.current) { ((constraints.maxHeight) / hourSize.count).toDp() } + is ScheduleSize.Adaptive -> with(LocalDensity.current) { + maxOf( + ((constraints.maxHeight) / numHours).toDp(), + hourSize.minSize + ) + } + } + Column(modifier = modifier) { + Row( + modifier = Modifier + .weight(1f) + .align(Alignment.Start) + ) { + ScheduleSidebar( + hourFormat = hourFormat, + hourHeight = hourHeight, + minTime = minTime, + maxTime = maxTime, + label = timeLabel, + modifier = Modifier + .verticalScroll(verticalScrollState) + .onGloballyPositioned { sidebarWidth = it.size.width } + ) + BasicSchedule( + hourFormat = hourFormat, + tasks = tasks, + taskContent = taskContent, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime, + minDate = minDate, + maxDate = maxDate, + minTime = minTime, + maxTime = maxTime, + dayWidth = dayWidth, + hourHeight = hourHeight, + modifier = Modifier + .weight(1f) + .verticalScroll(verticalScrollState) + ) + } + } + } +} + +@Composable +fun BasicSchedule( + hourFormat: Int, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int, + tasks: List, + modifier: Modifier = Modifier, + taskContent: @Composable (positionedTask: PositionedTask) -> Unit = { + BasicTask( + hourFormat = hourFormat, + positionedTask = it, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + }, + minDate: LocalDate = tasks.minByOrNull(Task::start)?.start?.date ?: Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()).date, + maxDate: LocalDate = tasks.map { + it.start.calculateEndTime( + focusSessions = it.focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + }.maxOfOrNull { it.date } ?: today().date, + minTime: LocalTime = min(), + maxTime: LocalTime = max(), + dayWidth: Dp, + hourHeight: Dp +) { + val numDays = differenceBetweenDays(minDate, maxDate) + 1 + val numMinutes = differenceBetweenMinutes(minTime, maxTime) + 1 + val dividerColor = + if (androidx.compose.material.MaterialTheme.colors.isLight) Color.LightGray else Color.DarkGray + val positionedTasks = + remember(tasks) { + arrangeTasks( + splitTasks( + tasks = tasks.sortedBy(Task::start), + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + ).filter { it.end > minTime && it.start < maxTime } + } + Layout( + content = { + positionedTasks.forEach { positionedTask -> + Box(modifier = Modifier.taskData(positionedTask)) { + taskContent(positionedTask) + } + } + }, + modifier = modifier + .offset(y = (16).dp) + .drawBehind { + repeat(23) { + drawLine( + dividerColor, + start = Offset(0f, (it + 1) * hourHeight.toPx()), + end = Offset(size.width, (it + 1) * hourHeight.toPx()), + strokeWidth = 1.dp.toPx() + ) + } + } + ) { measureables, constraints -> + val height = (hourHeight.toPx() * (numMinutes / 60f)).roundToInt() + val width = dayWidth.roundToPx() * numDays + val placeablesWithTasks = measureables.map { measurable -> + val splitTask = measurable.parentData as PositionedTask + val taskDurationMinutes = + differenceBetweenMinutes(splitTask.start, minOf(splitTask.end, maxTime)) + val taskHeight = ((taskDurationMinutes / 60f) * hourHeight.toPx()).roundToInt() + val taskWidth = + ((splitTask.colSpan.toFloat() / splitTask.colTotal.toFloat()) * dayWidth.toPx()).roundToInt() + val placeable = measurable.measure( + constraints.copy( + minWidth = taskWidth, + maxWidth = taskWidth, + minHeight = taskHeight, + maxHeight = taskHeight + ) + ) + Pair(placeable, splitTask) + } + layout(width, height) { + placeablesWithTasks.forEach { (placeable, splitTask) -> + val taskOffsetMinutes = if (splitTask.start > minTime) { + differenceBetweenMinutes( + minTime, + splitTask.start + ) + } else { + 0 + } + val taskY = ((taskOffsetMinutes / 60f) * hourHeight.toPx()).roundToInt() + val taskX = + (splitTask.col * (dayWidth.toPx() / splitTask.colTotal.toFloat())).roundToInt() + placeable.place(taskX, taskY) + } + } + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/calendar/CalendarScreenModel.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/calendar/CalendarScreenModel.kt new file mode 100644 index 0000000..a3b199a --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/calendar/CalendarScreenModel.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.calendar + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import com.joelkanyi.focusbloom.core.domain.repository.settings.SettingsRepository +import com.joelkanyi.focusbloom.core.domain.repository.tasks.TasksRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +class CalendarScreenModel( + tasksRepository: TasksRepository, + settingsRepository: SettingsRepository +) : ScreenModel { + private val _selectedDay = MutableStateFlow( + Clock.System.now().toLocalDateTime( + TimeZone.currentSystemDefault() + ).date + ) + val selectedDay = _selectedDay.stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = Clock.System.now().toLocalDateTime( + TimeZone.currentSystemDefault() + ).date + ) + + fun setSelectedDay(date: kotlinx.datetime.LocalDate) { + _selectedDay.value = date + } + + val tasks = tasksRepository.getTasks() + .map { tasks -> + tasks.sortedByDescending { + it.date + } + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = emptyList() + ) + + val hourFormat = settingsRepository.getHourFormat() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + val sessionTime = settingsRepository.getSessionTime() + .map { + it + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + val shortBreakTime = settingsRepository.getShortBreakTime() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + val longBreakTime = settingsRepository.getLongBreakTime() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/AllTasksScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/AllTasksScreen.kt new file mode 100644 index 0000000..fb8420a --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/AllTasksScreen.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar +import com.joelkanyi.focusbloom.core.presentation.component.TaskCard +import com.joelkanyi.focusbloom.feature.home.component.TaskOptionsBottomSheet +import com.joelkanyi.focusbloom.feature.taskprogress.TaskProgressScreen +import com.joelkanyi.focusbloom.platform.StatusBarColors +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +class AllTasksScreen : Screen, KoinComponent { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val screenModel = get() + StatusBarColors( + statusBarColor = MaterialTheme.colorScheme.background, + navBarColor = MaterialTheme.colorScheme.background + ) + val navigator = LocalNavigator.currentOrThrow + val tasksState = screenModel.tasks.collectAsState().value + val hourFormat = screenModel.hourFormat.collectAsState().value + val sessionTime = screenModel.sessionTime.collectAsState().value ?: 25 + val shortBreakTime = screenModel.shortBreakTime.collectAsState().value ?: 5 + val longBreakTime = screenModel.longBreakTime.collectAsState().value ?: 15 + val selectedTask = screenModel.selectedTask.collectAsState().value + val openBottomSheet = screenModel.openBottomSheet.collectAsState().value + val bottomSheetState = rememberModalBottomSheetState() + + if (openBottomSheet) { + if (selectedTask != null) { + TaskOptionsBottomSheet( + bottomSheetState = bottomSheetState, + onClickCancel = { + screenModel.openBottomSheet(false) + }, + onClickDelete = { + screenModel.deleteTask(it) + }, + onDismissRequest = { + screenModel.openBottomSheet(false) + screenModel.selectTask(null) + }, + onClickPushToTomorrow = { + screenModel.pushToTomorrow(it) + }, + onClickMarkAsCompleted = { + screenModel.markAsCompleted(it) + }, + task = selectedTask + ) + } + } + + AllTasksScreenContent( + tasksState = tasksState, + timeFormat = hourFormat, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime, + onClickNavigateBack = { + navigator.pop() + }, + onClickTaskOptions = { + screenModel.selectTask(it) + screenModel.openBottomSheet(true) + }, + onClickTask = { + navigator.push(TaskProgressScreen(taskId = it.id)) + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AllTasksScreenContent( + tasksState: TasksState, + timeFormat: Int, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int, + onClickNavigateBack: () -> Unit, + onClickTaskOptions: (task: Task) -> Unit, + onClickTask: (task: Task) -> Unit +) { + Box( + modifier = Modifier.fillMaxSize() + ) { + when (tasksState) { + is TasksState.Loading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } + + is TasksState.Success -> { + val tasks = tasksState.tasks + Scaffold( + topBar = { + BloomTopAppBar( + hasBackNavigation = true, + navigationIcon = { + IconButton(onClick = onClickNavigateBack) { + Icon( + imageVector = Icons.Outlined.ArrowBack, + contentDescription = "Back" + ) + } + } + ) { + Text(text = "Today's Tasks (${tasks.size})") + } + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(tasks) { + TaskCard( + task = it, + hourFormat = timeFormat, + onClick = onClickTask, + onShowTaskOption = onClickTaskOptions, + focusSessions = it.focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + } + } + } + } + } + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/HomeScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/HomeScreen.kt new file mode 100644 index 0000000..44f6f71 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/HomeScreen.kt @@ -0,0 +1,492 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.navigator.currentOrThrow +import com.joelkanyi.focusbloom.core.domain.model.SessionType +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.presentation.component.TaskCard +import com.joelkanyi.focusbloom.core.presentation.component.TaskProgress +import com.joelkanyi.focusbloom.core.presentation.theme.LongBreakColor +import com.joelkanyi.focusbloom.core.presentation.theme.SessionColor +import com.joelkanyi.focusbloom.core.presentation.theme.ShortBreakColor +import com.joelkanyi.focusbloom.core.utils.LocalAppNavigator +import com.joelkanyi.focusbloom.core.utils.pickFirstName +import com.joelkanyi.focusbloom.core.utils.sessionType +import com.joelkanyi.focusbloom.core.utils.taskCompleteMessage +import com.joelkanyi.focusbloom.core.utils.taskCompletionPercentage +import com.joelkanyi.focusbloom.core.utils.toTimer +import com.joelkanyi.focusbloom.feature.home.component.TaskOptionsBottomSheet +import com.joelkanyi.focusbloom.feature.taskprogress.TaskProgressScreen +import com.joelkanyi.focusbloom.feature.taskprogress.Timer +import com.joelkanyi.focusbloom.feature.taskprogress.TimerState +import com.joelkanyi.focusbloom.platform.StatusBarColors +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource +import org.koin.compose.rememberKoinInject + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen() { + val screenModel: HomeScreenModel = rememberKoinInject() + + StatusBarColors( + statusBarColor = MaterialTheme.colorScheme.background, + navBarColor = MaterialTheme.colorScheme.background + ) + val tasksState = screenModel.tasks.collectAsState().value + val username = screenModel.username.collectAsState().value ?: "" + val hourFormat = screenModel.hourFormat.collectAsState().value + val sessionTime = screenModel.sessionTime.collectAsState().value ?: 25 + val shortBreakTime = screenModel.shortBreakTime.collectAsState().value ?: 5 + val longBreakTime = screenModel.longBreakTime.collectAsState().value ?: 15 + val navigator = LocalAppNavigator.currentOrThrow + val selectedTask = screenModel.selectedTask.collectAsState().value + val openBottomSheet = screenModel.openBottomSheet.collectAsState().value + val shortBreakColor = screenModel.shortBreakColor.collectAsState().value + val longBreakColor = screenModel.longBreakColor.collectAsState().value + val focusColor = screenModel.focusColor.collectAsState().value + val timerState = Timer.timerState.collectAsState().value + val tickingTime = Timer.tickingTime.collectAsState().value + val bottomSheetState = rememberModalBottomSheetState() + + if (openBottomSheet) { + if (selectedTask != null) { + TaskOptionsBottomSheet( + bottomSheetState = bottomSheetState, + onClickCancel = { + screenModel.openBottomSheet(false) + }, + onClickDelete = { + screenModel.deleteTask(it) + }, + onDismissRequest = { + screenModel.openBottomSheet(false) + screenModel.selectTask(null) + }, + onClickPushToTomorrow = { + screenModel.pushToTomorrow(it) + }, + onClickMarkAsCompleted = { + screenModel.markAsCompleted(it) + }, + task = selectedTask + ) + } + } + + HomeScreenContent( + tasksState = tasksState, + timerState = timerState, + tickingTime = tickingTime, + focusTimeColor = focusColor, + shortBreakColor = shortBreakColor, + longBreakColor = longBreakColor, + hourFormat = hourFormat, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime, + username = username, + onClickTask = { + navigator.push(TaskProgressScreen(taskId = it.id)) + }, + onClickSeeAllTasks = { + navigator.push(AllTasksScreen()) + }, + onClickTaskOptions = { + screenModel.selectTask(it) + screenModel.openBottomSheet(true) + }, + onClickActiveTaskOptions = { + when (timerState) { + TimerState.Ticking -> { + Timer.pause() + } + + TimerState.Paused -> { + Timer.resume() + } + + else -> {} + } + } + ) +} + +@OptIn(ExperimentalResourceApi::class) +@Composable +private fun HomeScreenContent( + tasksState: TasksState, + timerState: TimerState, + tickingTime: Long, + hourFormat: Int, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int, + username: String, + onClickTask: (task: Task) -> Unit, + onClickSeeAllTasks: () -> Unit, + onClickTaskOptions: (task: Task) -> Unit, + focusTimeColor: Long?, + shortBreakColor: Long?, + longBreakColor: Long?, + onClickActiveTaskOptions: (task: Task) -> Unit +) { + Scaffold { paddingValues -> + Box( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + when (tasksState) { + TasksState.Loading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } + + is TasksState.Success -> { + val tasks = tasksState.tasks.sortedByDescending { it.completed.not() } + val activeTask = tasks.firstOrNull { it.active } + LazyColumn( + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + item { + if (activeTask != null) { + val containerColor = when (activeTask.current.sessionType()) { + SessionType.Focus -> { + if (focusTimeColor == null || focusTimeColor == 0L) { + Color(SessionColor) + } else { + Color( + focusTimeColor + ) + } + } + + SessionType.LongBreak -> { + if (longBreakColor == null || longBreakColor == 0L) { + Color(LongBreakColor) + } else { + Color( + longBreakColor + ) + } + } + + SessionType.ShortBreak -> { + if (shortBreakColor == null || shortBreakColor == 0L) { + Color(ShortBreakColor) + } else { + Color( + shortBreakColor + ) + } + } + } + ActiveTaskCard( + task = activeTask, + onClick = onClickTask, + containerColor = containerColor, + onClickTaskOptions = onClickActiveTaskOptions, + timerState = timerState, + tickingTime = tickingTime + ) + } + } + item { + Text( + text = "Hello, ${username.pickFirstName()}!", + style = MaterialTheme.typography.displaySmall + ) + } + item { + if (tasks.isNotEmpty() && tasks.all { it.completed }.not()) { + TodayTaskProgressCard(tasks = tasks) + } + } + if (tasks.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Today's Tasks (${tasks.size})", + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold + ) + ) + if (tasks.size > 3) { + TextButton(onClick = onClickSeeAllTasks) { + Text( + text = "See All", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + fontSize = 16.sp + ) + ) + } + } + } + } + } + if (tasks.all { it.completed }.not()) { + items(tasks.take(3)) { + TaskCard( + task = it, + onClick = onClickTask, + onShowTaskOption = onClickTaskOptions, + hourFormat = hourFormat, + focusSessions = it.focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + } + } + + if (tasks.all { it.completed } || tasks.isEmpty()) { + item { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = CenterHorizontally + ) { + Spacer(modifier = Modifier.height(24.dp)) + Image( + modifier = Modifier + .size(300.dp) + .align(CenterHorizontally), + painter = painterResource( + if (tasks.isEmpty()) "il_empty.xml" else "il_completed.xml" + ), + contentDescription = null + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .align(CenterHorizontally), + style = MaterialTheme.typography.titleSmall.copy( + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ), + text = if (tasks.isEmpty()) { + "Start your day productively! Add your first task." + } else if (tasks.all { it.completed }) { + "Great job! You've finished all your tasks for today." + } else { + "" + }, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + modifier = Modifier + .padding(horizontal = 24.dp) + .fillMaxWidth(), + text = if (tasks.isEmpty()) { + "To add a task, simply tap the '+' button at the bottom of the screen. Fill in the task details and tap 'Save'." + } else if (tasks.all { it.completed }) { + "Now, take some time to have fun, recharge, maybe do some exercise, and consider opening your calendar to plan for tomorrow's tasks. Keep up the fantastic work!" + } else { + "" + }, + style = MaterialTheme.typography.labelLarge.copy( + fontSize = 14.sp + ), + textAlign = TextAlign.Center + ) + } + } + } + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ActiveTaskCard( + task: Task, + timerState: TimerState, + tickingTime: Long, + onClick: (task: Task) -> Unit, + onClickTaskOptions: (task: Task) -> Unit, + containerColor: Color +) { + Card( + onClick = { + onClick(task) + }, + colors = CardDefaults.cardColors( + containerColor = containerColor + ) + ) { + Row( + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column( + modifier = Modifier + .fillMaxWidth(.85f), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = task.name, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onPrimary + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "${ + when (task.current.sessionType()) { + SessionType.Focus -> { + "Focus Session" + } + + SessionType.ShortBreak -> { + "Short Break" + } + + SessionType.LongBreak -> { + "Long Break" + } + } + } - ${ + tickingTime.toTimer() + }", + style = MaterialTheme.typography.labelMedium.copy( + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.SemiBold + ) + ) + } + IconButton( + onClick = { + onClickTaskOptions(task) + } + ) { + Icon( + modifier = Modifier, + imageVector = when (timerState) { + TimerState.Ticking -> { + Icons.Default.Pause + } + + TimerState.Paused -> { + Icons.Default.PlayArrow + } + + else -> { + Icons.Default.PlayArrow + } + }, + contentDescription = "Play/Pause", + tint = MaterialTheme.colorScheme.onPrimary + ) + } + } + } +} + +@Composable +private fun TodayTaskProgressCard(tasks: List) { + Card { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TaskProgress( + mainColor = MaterialTheme.colorScheme.primary, + percentage = taskCompletionPercentage(tasks).toFloat(), + counterColor = MaterialTheme.colorScheme.onSurface + ) + + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = taskCompleteMessage(tasks), + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold + ) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "${tasks.filter { it.completed }.size} of ${tasks.size} tasks completed", + style = MaterialTheme.typography.labelMedium.copy( + color = MaterialTheme.colorScheme.onSurface + ) + ) + } + } + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/HomeScreenModel.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/HomeScreenModel.kt new file mode 100644 index 0000000..5e3208e --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/HomeScreenModel.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.home + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.domain.repository.settings.SettingsRepository +import com.joelkanyi.focusbloom.core.domain.repository.tasks.TasksRepository +import com.joelkanyi.focusbloom.core.utils.plusDays +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +class HomeScreenModel( + private val tasksRepository: TasksRepository, + settingsRepository: SettingsRepository +) : ScreenModel { + private val _openBottomSheet = MutableStateFlow(false) + val openBottomSheet = _openBottomSheet.asStateFlow() + fun openBottomSheet(value: Boolean) { + _openBottomSheet.value = value + } + + val hourFormat = settingsRepository.getHourFormat() + .map { it ?: 24 } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = 24 + ) + + val sessionTime = settingsRepository.getSessionTime() + .map { + it + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + val shortBreakTime = settingsRepository.getShortBreakTime() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + val longBreakTime = settingsRepository.getLongBreakTime() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + fun deleteTask(task: Task) { + coroutineScope.launch { + tasksRepository.deleteTask(task.id) + } + } + + fun updateTask(task: Task) { + coroutineScope.launch { + tasksRepository.updateTask(task) + } + } + + private val _selectedTask = MutableStateFlow(null) + val selectedTask = _selectedTask.asStateFlow() + fun selectTask(task: Task?) { + _selectedTask.value = task + } + + fun pushToTomorrow(task: Task) { + coroutineScope.launch { + tasksRepository.updateTask( + task.copy( + date = task.date.plusDays(1), + start = task.start.plusDays(1) + ) + ) + } + } + + fun markAsCompleted(task: Task) { + coroutineScope.launch { + tasksRepository.updateTaskCompleted( + id = task.id, + completed = true + ) + tasksRepository.updateTaskActive( + id = task.id, + active = false + ) + tasksRepository.updateTaskInProgress( + id = task.id, + inProgressTask = false + ) + } + } + + val tasks = tasksRepository.getTasks() + .map { tasks -> + TasksState.Success( + tasks + .sortedBy { it.start } + .filter { + it.date.date == Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()).date // .minusDays(1) + } + ) + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = TasksState.Loading + ) + + val username = settingsRepository.getUsername() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + val shortBreakColor = settingsRepository.shortBreakColor() + .map { it } + .stateIn( + coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + val longBreakColor = settingsRepository.longBreakColor() + .map { it } + .stateIn( + coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + val focusColor = settingsRepository.focusColor() + .map { it } + .stateIn( + coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) +} + +sealed class TasksState { + data object Loading : TasksState() + data class Success(val tasks: List) : TasksState() +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/component/Option.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/component/Option.kt new file mode 100644 index 0000000..193fa29 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/component/Option.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.home.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +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.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun Option(icon: ImageVector, text: String, onClick: () -> Unit) { + Row( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .clickable { onClick() }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + modifier = Modifier, + imageVector = icon, + contentDescription = text, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + Text( + modifier = Modifier, + text = text, + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp + ) + ) + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/component/TaskOptionsBottomSheet.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/component/TaskOptionsBottomSheet.kt new file mode 100644 index 0000000..86cb25b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/home/component/TaskOptionsBottomSheet.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.home.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material.icons.outlined.EditCalendar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.joelkanyi.focusbloom.core.domain.model.Task + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TaskOptionsBottomSheet( + bottomSheetState: SheetState, + onClickCancel: (task: Task) -> Unit, + onClickDelete: (task: Task) -> Unit, + onClickPushToTomorrow: (task: Task) -> Unit, + task: Task, + onDismissRequest: () -> Unit, + onClickMarkAsCompleted: (task: Task) -> Unit +) { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = bottomSheetState + ) { + Column( + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Option( + icon = Icons.Default.Edit, + text = "Edit Task", + onClick = { + onDismissRequest() + } + ) + Option( + icon = Icons.Outlined.EditCalendar, + text = "Push to Tomorrow", + onClick = { + onClickPushToTomorrow(task) + onDismissRequest() + } + ) + Option( + icon = Icons.Outlined.Done, + text = "Mark as Completed", + onClick = { + onClickMarkAsCompleted(task) + onDismissRequest() + } + ) + Option( + icon = Icons.Outlined.Delete, + text = "Delete Task", + onClick = { + onClickDelete(task) + onDismissRequest() + } + ) + Option( + icon = Icons.Outlined.Close, + text = "Cancel", + onClick = { + onClickCancel(task) + onDismissRequest() + } + ) + Spacer(modifier = Modifier.height(32.dp)) + } + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/onboarding/OnboadingViewModel.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/onboarding/OnboadingViewModel.kt new file mode 100644 index 0000000..2c85e45 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/onboarding/OnboadingViewModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.onboarding + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import com.joelkanyi.focusbloom.core.domain.repository.settings.SettingsRepository +import com.joelkanyi.focusbloom.core.utils.UiEvents +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class OnboadingViewModel( + private val settingsRepository: SettingsRepository +) : ScreenModel { + private val _eventsFlow = MutableSharedFlow() + val eventsFlow = _eventsFlow.asSharedFlow() + + private val _username = MutableStateFlow("") + val username = _username.asStateFlow() + fun setUsername(username: String) { + _username.value = username + } + + fun saveUsername() { + coroutineScope.launch { + settingsRepository.saveUsername(username.value.trim()) + _eventsFlow.emit(UiEvents.Navigation) + } + } + + val typeWriterTextParts = listOf( + "be focused", + "be present", + "concentrate", + "be effective", + "be productive", + "get things done", + "be efficient", + "be organized", + "be intentional", + "be disciplined", + "be motivated", + "be consistent", + "be mindful" + ) +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/onboarding/OnboardingScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/onboarding/OnboardingScreen.kt new file mode 100644 index 0000000..d90a378 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/onboarding/OnboardingScreen.kt @@ -0,0 +1,238 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.onboarding + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource +import org.koin.core.component.KoinComponent + +class OnboardingScreen : Screen, KoinComponent { + @OptIn(ExperimentalFoundationApi::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val coroutineScope = rememberCoroutineScope() + val pageCount = 3 + val pagerState = rememberPagerState(pageCount = { pageCount }) + + OnboardingScreenContent( + pagerState = pagerState, + pageCount = pageCount, + onClickNext = { + coroutineScope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + }, + onClickGetStarted = { + navigator.push(UsernameScreen()) + } + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun OnboardingScreenContent( + pageCount: Int, + pagerState: PagerState, + onClickNext: () -> Unit, + onClickGetStarted: () -> Unit +) { + Scaffold( + bottomBar = { + if (pagerState.currentPage == pageCount - 1) { + OnBoardingNavigationButton( + modifier = Modifier.padding(16.dp), + text = "Get Started", + onClick = onClickGetStarted + ) + } else { + OnBoardingNavigationButton( + modifier = Modifier.padding(16.dp), + text = "Next", + onClick = onClickNext + ) + } + } + ) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues) + ) { + HorizontalPager( + modifier = Modifier + .weight(.9f) + .padding(16.dp), + state = pagerState + ) { currentPage -> + when (currentPage) { + 0 -> OnboardingFirstPage() + 1 -> OnboardingSecondPage() + 2 -> OnboardingThirdPage() + } + } + + PageIndicators( + pageCount = pageCount, + currentPage = pagerState.currentPage + ) + } + } +} + +@Composable +private fun ColumnScope.PageIndicators(pageCount: Int, currentPage: Int) { + Row( + Modifier + .weight(.1f) + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + repeat(pageCount) { iteration -> + val color = + if (currentPage == iteration) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.2f + ) + } + Box( + modifier = Modifier + .padding(2.dp) + .clip(CircleShape) + .background(color) + .width(24.dp) + .height(8.dp) + + ) + } + } +} + +@Composable +private fun OnboardingFirstPage() { + PageContent( + title = "Organize Tasks and Boost Productivity", + description = "Welcome to FocusBloom, your task management and productivity companion. Effortlessly organize your tasks and supercharge your productivity journey.", + illustration = "il_tasks.xml" + ) +} + +@Composable +private fun OnboardingSecondPage() { + PageContent( + title = "Tailor Your Work Sessions", + description = "With FocusBloom, you have the power to customize your work and break durations to match your preferences and maximize efficiency.", + illustration = "il_work_time.xml" + ) +} + +@Composable +private fun OnboardingThirdPage() { + PageContent( + title = "Visualize Your Progress", + description = "Experience the power of progress tracking with FocusBloom. Gain insights into your productivity journey and visualize task completion trends.", + illustration = "il_statistics.xml" + ) +} + +@OptIn(ExperimentalResourceApi::class) +@Composable +private fun PageContent(title: String, description: String, illustration: String) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(illustration), + contentDescription = illustration, + modifier = Modifier.size(370.dp) + ) + Spacer(modifier = Modifier.height(32.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = title, + style = MaterialTheme.typography.titleLarge.copy( + fontSize = 22.sp, + textAlign = TextAlign.Center + ) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = description, + style = MaterialTheme.typography.labelMedium.copy( + fontSize = 14.sp, + textAlign = TextAlign.Center + ) + ) + } +} + +@Composable +fun OnBoardingNavigationButton(modifier: Modifier = Modifier, text: String, onClick: () -> Unit) { + Button( + modifier = modifier + .fillMaxWidth() + .height(56.dp), + onClick = onClick, + shape = MaterialTheme.shapes.medium + ) { + Text( + text = text, + style = MaterialTheme.typography.titleLarge.copy( + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + ) + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/onboarding/UsernameScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/onboarding/UsernameScreen.kt new file mode 100644 index 0000000..1961d9c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/onboarding/UsernameScreen.kt @@ -0,0 +1,256 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.onboarding + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.joelkanyi.focusbloom.core.utils.UiEvents +import com.joelkanyi.focusbloom.main.MainScreen +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +class UsernameScreen : Screen, KoinComponent { + + @OptIn(ExperimentalComposeUiApi::class) + @Composable + override fun Content() { + val onboadingViewModel = get() + val navigator = LocalNavigator.currentOrThrow + val username = onboadingViewModel.username.collectAsState().value + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + onboadingViewModel.eventsFlow.collectLatest { event -> + when (event) { + is UiEvents.Navigation -> { + navigator.replaceAll(MainScreen()) + } + + else -> {} + } + } + } + UsernameScreenContent( + username = username, + typeWriterTextParts = onboadingViewModel.typeWriterTextParts, + onUsernameChange = { + onboadingViewModel.setUsername(it) + }, + onClickContinue = { + keyboardController?.hide() + onboadingViewModel.saveUsername() + } + ) + } +} + +@Composable +fun UsernameScreenContent( + username: String, + typeWriterTextParts: List, + onUsernameChange: (String) -> Unit, + onClickContinue: () -> Unit +) { + LazyColumn( + modifier = Modifier + .padding(16.dp) + .fillMaxSize() + ) { + item { + TypewriterText( + baseText = "Focus Bloom app is what you need to", + parts = typeWriterTextParts + ) + } + + item { + Spacer(modifier = Modifier.height(56.dp)) + Text( + text = "What's your username?", + style = MaterialTheme.typography.labelLarge.copy( + fontSize = 18.sp + ) + ) + } + + item { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + UsernameTextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + name = username, + onNameChange = { + onUsernameChange(it) + }, + onClickDone = { + onClickContinue() + } + ) + } + + item { + AnimatedVisibility( + username.isNotEmpty() + ) { + Column { + Spacer(modifier = Modifier.height(56.dp)) + Button( + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + shape = MaterialTheme.shapes.medium, + onClick = onClickContinue + ) { + Text( + text = "Continue", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ) + ) + } + } + } + } + } +} + +@Composable +private fun UsernameTextField( + modifier: Modifier, + name: String, + onNameChange: (String) -> Unit, + onClickDone: () -> Unit +) { + TextField( + modifier = modifier, + value = name, + onValueChange = onNameChange, + maxLines = 1, + singleLine = true, + placeholder = { + Text( + text = "John Doe", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.ExtraLight, + fontSize = 18.sp, + letterSpacing = -(1.6).sp, + lineHeight = 32.sp + ) + ) + }, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = MaterialTheme.colorScheme.background, + focusedContainerColor = MaterialTheme.colorScheme.background, + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.primary, + disabledIndicatorColor = MaterialTheme.colorScheme.primary + ), + textStyle = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp + ), + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Text, + capitalization = KeyboardCapitalization.Words + ), + keyboardActions = KeyboardActions( + onDone = { + onClickDone() + } + ) + ) +} + +@Composable +private fun TypewriterText(modifier: Modifier = Modifier, baseText: String, parts: List) { + var partIndex by remember { mutableStateOf(0) } + var partText by remember { mutableStateOf("") } + val textToDisplay = "$baseText $partText" + Text( + modifier = modifier, + text = textToDisplay, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + letterSpacing = -(1.6).sp, + lineHeight = 32.sp + ) + ) + + LaunchedEffect(key1 = parts) { + while (partIndex <= parts.size) { + val part = parts[partIndex] + + part.forEachIndexed { charIndex, _ -> + partText = part.substring(startIndex = 0, endIndex = charIndex + 1) + delay(100) + } + + delay(1500) + + part.forEachIndexed { charIndex, _ -> + partText = part + .substring(startIndex = 0, endIndex = part.length - (charIndex + 1)) + delay(30) + } + + delay(500) + + partIndex = (partIndex + 1) % parts.size + } + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/settings/SettingsScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/settings/SettingsScreen.kt new file mode 100644 index 0000000..d491297 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/settings/SettingsScreen.kt @@ -0,0 +1,843 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.settings + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.HourglassEmpty +import androidx.compose.material.icons.outlined.LightMode +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.outlined.Timer +import androidx.compose.material.icons.outlined.VolumeUp +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material.icons.rounded.KeyboardArrowUp +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.joelkanyi.focusbloom.core.domain.model.TextFieldState +import com.joelkanyi.focusbloom.core.presentation.component.BloomDropDown +import com.joelkanyi.focusbloom.core.presentation.component.BloomInputTextField +import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar +import com.joelkanyi.focusbloom.core.presentation.theme.Blue +import com.joelkanyi.focusbloom.core.presentation.theme.Green +import com.joelkanyi.focusbloom.core.presentation.theme.LightBlue +import com.joelkanyi.focusbloom.core.presentation.theme.LightGreen +import com.joelkanyi.focusbloom.core.presentation.theme.LongBreakColor +import com.joelkanyi.focusbloom.core.presentation.theme.Orange +import com.joelkanyi.focusbloom.core.presentation.theme.Pink +import com.joelkanyi.focusbloom.core.presentation.theme.Red +import com.joelkanyi.focusbloom.core.presentation.theme.SessionColor +import com.joelkanyi.focusbloom.core.presentation.theme.ShortBreakColor +import com.joelkanyi.focusbloom.core.presentation.theme.Yellow +import com.joelkanyi.focusbloom.core.utils.isDigitsOnly +import com.joelkanyi.focusbloom.core.utils.timeFormat +import com.joelkanyi.focusbloom.platform.StatusBarColors +import org.koin.compose.rememberKoinInject + +@Composable +fun SettingsScreen() { + val screenModel: SettingsScreenModel = rememberKoinInject() + + val darkTheme = when (screenModel.appTheme.collectAsState().value) { + 1 -> true + else -> false + } + StatusBarColors( + statusBarColor = MaterialTheme.colorScheme.background, + navBarColor = MaterialTheme.colorScheme.background + ) + + val sessionTime = screenModel.sessionTime.collectAsState().value ?: 25 + val shortBreakTime = screenModel.shortBreakTime.collectAsState().value ?: 5 + val longBreakTime = screenModel.longBreakTime.collectAsState().value ?: 15 + val timeFormat = screenModel.timeFormat.collectAsState().value ?: 24 + val selectedColorCardTitle = screenModel.selectedColorCardTitle.collectAsState().value + val currentShortBreakColor = screenModel.shortBreakColor.collectAsState().value + val currentLongBreakColor = screenModel.longBreakColor.collectAsState().value + val currentSessionColor = screenModel.focusColor.collectAsState().value + val showColorDialog = screenModel.showColorDialog.collectAsState().value + + SettingsScreenContent( + darkTheme = darkTheme, + onDarkThemeChange = { themeValue -> + screenModel.setAppTheme(if (themeValue) 1 else 0) + }, + optionsOpened = screenModel.optionsOpened, + openOptions = { option -> + screenModel.openOptions(option) + }, + focusSessionMinutes = sessionTime, + onFocusSessionMinutesChange = { time -> + if (time.isEmpty()) { + screenModel.setSessionTime(0) + return@SettingsScreenContent + } + if (time.isDigitsOnly().not()) { + return@SettingsScreenContent + } + screenModel.setSessionTime(time.toInt()) + }, + shortBreakMinutes = shortBreakTime, + onShortBreakMinutesChange = { time -> + if (time.isEmpty()) { + screenModel.setShortBreakTime(0) + return@SettingsScreenContent + } + if (time.isDigitsOnly().not()) { + return@SettingsScreenContent + } + screenModel.setShortBreakTime(time.toInt()) + }, + longBreakMinutes = longBreakTime, + onLongBreakMinutesChange = { time -> + if (time.isEmpty()) { + screenModel.setLongBreakTime(0) + return@SettingsScreenContent + } + if (time.isDigitsOnly().not()) { + return@SettingsScreenContent + } + screenModel.setLongBreakTime(time.toInt()) + }, + hourFormats = screenModel.hourFormats, + selectedHourFormat = timeFormat, + onHourFormatChange = { + screenModel.setHourFormat(it) + }, + showColorDialog = showColorDialog, + selectedColorCardTitle = selectedColorCardTitle, + onColorCardTitleChange = { + screenModel.setSelectedColorCardTitle(it) + }, + onShowColorDialog = { + screenModel.setShowColorDialog(it) + }, + currentShortBreakColor = if (currentShortBreakColor?.toInt() == 0 || currentShortBreakColor == null) { + ShortBreakColor + } else { + currentShortBreakColor + }, + currentLongBreakColor = if (currentLongBreakColor?.toInt() == 0 || currentLongBreakColor == null) { + LongBreakColor + } else { + currentLongBreakColor + }, + currentSessionColor = if (currentSessionColor?.toInt() == 0 || currentSessionColor == null) { + SessionColor + } else { + currentSessionColor + }, + onSelectColor = { + when (selectedColorCardTitle) { + "Focus Session" -> { + screenModel.setFocusColor(it) + } + + "Short Break" -> { + screenModel.setShortBreakColor(it) + } + + "Long Break" -> { + screenModel.setLongBreakColor(it) + } + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreenContent( + optionsOpened: List, + openOptions: (String) -> Unit, + focusSessionMinutes: Int, + onFocusSessionMinutesChange: (String) -> Unit, + shortBreakMinutes: Int, + onShortBreakMinutesChange: (String) -> Unit, + longBreakMinutes: Int, + onLongBreakMinutesChange: (String) -> Unit, + hourFormats: List, + selectedHourFormat: Int, + onHourFormatChange: (Int) -> Unit, + showColorDialog: Boolean, + selectedColorCardTitle: String, + onColorCardTitleChange: (String) -> Unit, + onShowColorDialog: (Boolean) -> Unit, + darkTheme: Boolean, + onDarkThemeChange: (Boolean) -> Unit, + currentShortBreakColor: Long, + currentLongBreakColor: Long, + currentSessionColor: Long, + onSelectColor: (Long) -> Unit +) { + Scaffold( + topBar = { + BloomTopAppBar( + hasBackNavigation = false + ) { + Text(text = "Settings") + } + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + item { + FocusSessionsSetting( + focusSessionMinutes = focusSessionMinutes, + onFocusSessionMinutesChange = onFocusSessionMinutesChange, + shortBreakMinutes = shortBreakMinutes, + onShortBreakMinutesChange = onShortBreakMinutesChange, + longBreakMinutes = longBreakMinutes, + onLongBreakMinutesChange = onLongBreakMinutesChange, + expanded = { title -> + optionsOpened.contains(title) + }, + onExpand = { title -> + openOptions(title) + } + ) + } + item { + TimeSetting( + expanded = { title -> + optionsOpened.contains(title) + }, + onExpand = { title -> + openOptions(title) + }, + hourFormats = hourFormats, + selectedHourFormat = selectedHourFormat, + onHourFormatChange = onHourFormatChange + ) + } + item { + SoundSetting( + expanded = { title -> + optionsOpened.contains(title) + }, + onExpand = { title -> + openOptions(title) + } + ) + } + item { + ThemeSetting( + expanded = { title -> + optionsOpened.contains(title) + }, + onExpand = { title -> + openOptions(title) + }, + showColorDialog = showColorDialog, + selectedColorCardTitle = selectedColorCardTitle, + onColorCardTitleChange = onColorCardTitleChange, + onShowColorDialog = onShowColorDialog, + darkTheme = darkTheme, + onDarkThemeChange = onDarkThemeChange, + currentShortBreakColor = currentShortBreakColor, + currentLongBreakColor = currentLongBreakColor, + currentSessionColor = currentSessionColor, + onSelectColor = onSelectColor + ) + } + item { + NotificationsSetting( + expanded = { title -> + optionsOpened.contains(title) + }, + onExpand = { title -> + openOptions(title) + } + ) + } + } + } +} + +@Composable +fun FocusSessionsSetting( + focusSessionMinutes: Int, + onFocusSessionMinutesChange: (String) -> Unit, + shortBreakMinutes: Int, + onShortBreakMinutesChange: (String) -> Unit, + longBreakMinutes: Int, + onLongBreakMinutesChange: (String) -> Unit, + onExpand: (String) -> Unit, + expanded: (String) -> Boolean +) { + SettingCard( + onExpand = { + onExpand("Focus Sessions") + }, + expanded = expanded("Focus Sessions"), + title = "Focus Sessions", + icon = Icons.Outlined.HourglassEmpty, + content = { + var autoStartBreaks by remember { mutableStateOf(false) } + var autoStartFocusSession by remember { mutableStateOf(false) } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + SessionTime( + modifier = Modifier.weight(1f), + title = "Session", + currentValue = focusSessionMinutes.toString(), + onValueChange = { + onFocusSessionMinutesChange(it) + } + ) + SessionTime( + modifier = Modifier.weight(1f), + title = "Short Break", + currentValue = shortBreakMinutes.toString(), + onValueChange = { + onShortBreakMinutesChange(it) + } + ) + SessionTime( + modifier = Modifier.weight(1f), + title = "Long Break", + currentValue = longBreakMinutes.toString(), + onValueChange = { + onLongBreakMinutesChange(it) + } + ) + } + Spacer(modifier = Modifier.height(12.dp)) + AutoStartSession( + title = "Auto Start Breaks", + checked = autoStartBreaks, + onCheckedChange = { + autoStartBreaks = it + } + ) + Spacer(modifier = Modifier.height(12.dp)) + AutoStartSession( + title = "Auto Start Sessions", + checked = autoStartFocusSession, + onCheckedChange = { + autoStartFocusSession = it + } + ) + } + ) +} + +@Composable +fun TimeSetting( + onExpand: (String) -> Unit, + expanded: (String) -> Boolean, + hourFormats: List, + selectedHourFormat: Int, + onHourFormatChange: (Int) -> Unit +) { + SettingCard( + onExpand = { + onExpand("Time") + }, + expanded = expanded("Time"), + title = "Time", + icon = Icons.Outlined.Timer, + content = { + SoundSelection( + title = "Hour Format", + options = hourFormats, + selectedOption = selectedHourFormat.timeFormat(), + onSelectOption = { + onHourFormatChange(it.timeFormat()) + onExpand("Time") + } + ) + } + ) +} + +@Composable +fun SoundSetting(onExpand: (String) -> Unit, expanded: (String) -> Boolean) { + SettingCard( + onExpand = { + onExpand("Sounds") + }, + expanded = expanded("Sounds"), + title = "Sounds", + icon = Icons.Outlined.VolumeUp, + content = { + var alarmSliderPosition by remember { mutableStateOf(0f) } + var tickingSliderPosition by remember { mutableStateOf(0f) } + var selectedAlarmSound by remember { + mutableStateOf("Nokia Tune") + } + var selectedTickingSound by remember { + mutableStateOf("White Noise") + } + SoundSelection( + title = "Alarm Sounds", + options = listOf("Nokia Tune", "Samsung Tune", "Itel Tune", "Oppo Tune"), + selectedOption = selectedAlarmSound, + onSelectOption = { + selectedAlarmSound = it + } + ) + Slider( + value = alarmSliderPosition, + valueRange = 0f..100f, + onValueChange = { alarmSliderPosition = it }, + colors = SliderDefaults.colors( + inactiveTickColor = MaterialTheme.colorScheme.secondary, + inactiveTrackColor = MaterialTheme.colorScheme.secondary + ) + ) + Spacer(modifier = Modifier.height(16.dp)) + SoundSelection( + title = "Ticking Sounds", + options = listOf("White Noise", "Clock Ticking"), + selectedOption = selectedTickingSound, + onSelectOption = { + selectedTickingSound = it + } + ) + Slider( + value = tickingSliderPosition, + valueRange = 0f..100f, + onValueChange = { tickingSliderPosition = it }, + colors = SliderDefaults.colors( + inactiveTickColor = MaterialTheme.colorScheme.secondary, + inactiveTrackColor = MaterialTheme.colorScheme.secondary + ) + ) + } + ) +} + +@Composable +fun ThemeSetting( + onExpand: (String) -> Unit, + expanded: (String) -> Boolean, + showColorDialog: Boolean, + selectedColorCardTitle: String, + onColorCardTitleChange: (String) -> Unit, + onShowColorDialog: (Boolean) -> Unit, + darkTheme: Boolean, + onDarkThemeChange: (Boolean) -> Unit, + currentShortBreakColor: Long, + currentLongBreakColor: Long, + currentSessionColor: Long, + onSelectColor: (Long) -> Unit +) { + SettingCard( + onExpand = { + onExpand("Theme") + }, + expanded = expanded("Theme"), + title = "Theme", + icon = Icons.Outlined.LightMode, + content = { + if (showColorDialog) { + ColorsDialog( + title = "Choose $selectedColorCardTitle Color", + onDismiss = { + onShowColorDialog(false) + }, + onSelectColor = { + onShowColorDialog(false) + onSelectColor(it) + } + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "Sessions Color Scheme") + ColorsSelection( + onSelectSessionColor = { + onShowColorDialog(true) + onColorCardTitleChange("Focus Session") + }, + onSelectShortBreakColor = { + onShowColorDialog(true) + onColorCardTitleChange("Short Break") + }, + onSelectLongBreakColor = { + onShowColorDialog(true) + onColorCardTitleChange("Long Break") + }, + currentSessionColor = currentSessionColor, + currentShortBreakColor = currentShortBreakColor, + currentLongBreakColor = currentLongBreakColor + ) + } + Spacer(modifier = Modifier.height(16.dp)) + AutoStartSession( + title = "App Theme (${ + if (darkTheme) { + "Dark" + } else { + "Light" + } + })", + checked = darkTheme, + onCheckedChange = { + onDarkThemeChange(it) + } + ) + } + ) +} + +@Composable +fun NotificationsSetting(onExpand: (String) -> Unit, expanded: (String) -> Boolean) { + SettingCard( + onExpand = { + onExpand("Notifications") + }, + expanded = expanded("Notifications"), + title = "Notifications", + icon = Icons.Outlined.Notifications, + content = { + var selectedReminderType by remember { + mutableStateOf("Both") + } + var howManyMinutesToReminder by remember { + mutableStateOf("5") + } + Row { + Text( + modifier = Modifier.fillMaxWidth(.4f), + text = "Reminder" + ) + Spacer(modifier = Modifier.height(12.dp)) + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.End + ) { + BloomDropDown( + options = listOf("Focus Session", "Break", "Both", "None"), + selectedOption = TextFieldState(selectedReminderType), + onOptionSelected = { + selectedReminderType = it + } + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + BloomInputTextField( + modifier = Modifier.weight(1f), + value = TextFieldState(text = howManyMinutesToReminder), + onValueChange = { + howManyMinutesToReminder = it + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = "min") + } + } + } + } + ) +} + +@Composable +private fun SoundSelection( + options: List, + title: String, + selectedOption: String, + onSelectOption: (String) -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.fillMaxWidth(.5f), + text = title + ) + BloomDropDown( + modifier = Modifier + .fillMaxWidth(), + options = options, + selectedOption = TextFieldState(text = selectedOption), + onOptionSelected = { + onSelectOption(it) + } + ) + } +} + +@Composable +fun AutoStartSession(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = title) + Switch( + checked = checked, + onCheckedChange = onCheckedChange + ) + } +} + +@Composable +fun SessionTime( + modifier: Modifier = Modifier, + title: String, + currentValue: String, + onValueChange: (String) -> Unit +) { + BloomInputTextField( + modifier = modifier, + textStyle = MaterialTheme.typography.bodyMedium.copy( + textAlign = TextAlign.Start + ), + label = { + Text( + text = title, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ) + ) + }, + value = TextFieldState(currentValue), + onValueChange = onValueChange, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number + ) + ) +} + +@Composable +fun SettingCard( + title: String, + icon: ImageVector, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, + onExpand: () -> Unit, + expanded: Boolean +) { + Card(modifier = modifier) { + Column( + modifier = modifier.padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + imageVector = icon, + contentDescription = title + ) + Text( + text = title, + style = MaterialTheme.typography.titleLarge + ) + } + + IconButton(onClick = { onExpand() }) { + Icon( + imageVector = if (expanded) { + Icons.Rounded.KeyboardArrowUp + } else { + Icons.Rounded.KeyboardArrowDown + }, + contentDescription = null + ) + } + } + AnimatedVisibility(expanded) { + Column { + Spacer(modifier = Modifier.height(8.dp)) + content() + } + } + } + } +} + +@Composable +fun ColorsSelection( + onSelectSessionColor: (Long) -> Unit, + onSelectShortBreakColor: (Long) -> Unit, + onSelectLongBreakColor: (Long) -> Unit, + currentSessionColor: Long, + currentShortBreakColor: Long, + currentLongBreakColor: Long +) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + ColorCard( + color = currentSessionColor, + onClick = onSelectSessionColor + ) + ColorCard( + color = currentShortBreakColor, + onClick = onSelectShortBreakColor + ) + ColorCard( + color = currentLongBreakColor, + onClick = onSelectLongBreakColor + ) + } +} + +@Composable +fun ColorCard(modifier: Modifier = Modifier, color: Long, onClick: (Long) -> Unit) { + Box( + modifier = modifier + .size(32.dp) + .clip(MaterialTheme.shapes.medium) + .background(Color(color)) + .clickable { + onClick(color) + } + ) +} + +@Composable +fun ColorsDialog( + modifier: Modifier = Modifier, + onDismiss: () -> Unit, + onSelectColor: (Long) -> Unit, + title: String +) { + AlertDialog( + modifier = modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + icon = {}, + containerColor = MaterialTheme.colorScheme.background, + onDismissRequest = onDismiss, + title = { + Text( + modifier = Modifier.fillMaxWidth(), + text = title, + style = MaterialTheme.typography.titleMedium.copy( + textAlign = TextAlign.Center + ) + ) + }, + text = { + LazyVerticalGrid( + columns = GridCells.Fixed(4), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(sessionColors) { + ColorCard( + modifier = Modifier + .padding(horizontal = 4.dp) + .size(48.dp), + color = it, + onClick = onSelectColor + ) + } + } + }, + dismissButton = {}, + confirmButton = {} + ) +} + +private val sessionColors = listOf( + SessionColor, + ShortBreakColor, + LongBreakColor, + Red, + Green, + Orange, + Blue, + Green, + LightGreen, + Yellow, + LightBlue, + Pink +) + +/** + * Settings + * Focus Sessions + * - time for short, long and focus session + * - auto start breaks, sessions + * Sounds + * - alarm sound - repeat times + * - ticking sound + * Theme + * - for breaks and focus session + * Hour format + * Notification + * - reminder - last, middle task + * - how many minutes to + * - alarm name + * - add new one + */ diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/settings/SettingsScreenModel.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/settings/SettingsScreenModel.kt new file mode 100644 index 0000000..55c1b8b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/settings/SettingsScreenModel.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.settings + +import androidx.compose.runtime.mutableStateListOf +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import com.joelkanyi.focusbloom.core.domain.repository.settings.SettingsRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class SettingsScreenModel( + private val settingsRepository: SettingsRepository +) : ScreenModel { + private val _selectedColorCardTitle = MutableStateFlow("") + val selectedColorCardTitle = _selectedColorCardTitle.asStateFlow() + fun setSelectedColorCardTitle(title: String) { + _selectedColorCardTitle.value = title + } + + val hourFormats: List = listOf("12-hour", "24-hour") + + val optionsOpened = mutableStateListOf("") + fun openOptions(option: String) { + if (optionsOpened.contains(option)) { + optionsOpened.remove(option) + } else { + optionsOpened.add(option) + } + } + + val appTheme: StateFlow = settingsRepository.getAppTheme() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + fun setAppTheme(appTheme: Int) { + coroutineScope.launch { + settingsRepository.saveAppTheme(appTheme) + } + } + + val sessionTime = settingsRepository.getSessionTime() + .map { + it + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + fun setSessionTime(sessionTime: Int) { + coroutineScope.launch { + settingsRepository.saveSessionTime(sessionTime) + } + } + + val shortBreakTime = settingsRepository.getShortBreakTime() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + fun setShortBreakTime(shortBreakTime: Int) { + coroutineScope.launch { + settingsRepository.saveShortBreakTime(shortBreakTime) + } + } + + val longBreakTime = settingsRepository.getLongBreakTime() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + fun setLongBreakTime(longBreakTime: Int) { + coroutineScope.launch { + settingsRepository.saveLongBreakTime(longBreakTime) + } + } + + val timeFormat = settingsRepository.getHourFormat() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + fun setHourFormat(timeFormat: Int) { + coroutineScope.launch { + settingsRepository.saveHourFormat(timeFormat) + } + } + + fun setShortBreakColor(color: Long) { + coroutineScope.launch { + settingsRepository.saveShortBreakColor(color) + } + } + + fun setLongBreakColor(color: Long) { + coroutineScope.launch { + settingsRepository.saveLongBreakColor(color) + } + } + + fun setFocusColor(color: Long) { + coroutineScope.launch { + settingsRepository.saveFocusColor(color) + } + } + + private val _showColorDialog = MutableStateFlow(false) + val showColorDialog = _showColorDialog.asStateFlow() + fun setShowColorDialog(it: Boolean) { + _showColorDialog.value = it + } + + val shortBreakColor = settingsRepository.shortBreakColor() + .map { + it + } + .stateIn( + coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + val longBreakColor = settingsRepository.longBreakColor() + .map { it } + .stateIn( + coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + val focusColor = settingsRepository.focusColor() + .map { it } + .stateIn( + coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/AllStatisticsScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/AllStatisticsScreen.kt new file mode 100644 index 0000000..9008115 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/AllStatisticsScreen.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.statistics + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar +import com.joelkanyi.focusbloom.core.utils.prettyFormat +import com.joelkanyi.focusbloom.platform.StatusBarColors +import kotlinx.datetime.LocalDate +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +class AllStatisticsScreen : Screen, KoinComponent { + + @Composable + override fun Content() { + val screenModel = get() + StatusBarColors( + statusBarColor = MaterialTheme.colorScheme.background, + navBarColor = MaterialTheme.colorScheme.background + ) + val navigator = LocalNavigator.currentOrThrow + val hourFormat = screenModel.hourFormat.collectAsState().value ?: 24 + val sessionTime = screenModel.sessionTime.collectAsState().value ?: 25 + val shortBreakTime = screenModel.shortBreakTime.collectAsState().value ?: 5 + val longBreakTime = screenModel.longBreakTime.collectAsState().value ?: 15 + val tasksHistory = screenModel.tasks.collectAsState().value.groupBy { it.date.date } + + AllStatisticsScreenContent( + timeFormat = hourFormat, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime, + tasks = tasksHistory, + onClickNavigateBack = { + navigator.pop() + }, + onClickDelete = { + screenModel.deleteTask(it) + }, + showTaskOption = { + screenModel.openedTasks.contains(it) + }, + onShowTaskOption = { + screenModel.openTaskOptions(it) + }, + onClickCancel = { + screenModel.openTaskOptions(it) + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +fun AllStatisticsScreenContent( + timeFormat: Int, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int, + tasks: Map>, + onClickNavigateBack: () -> Unit, + onClickDelete: (task: Task) -> Unit, + onClickCancel: (task: Task) -> Unit, + showTaskOption: (task: Task) -> Boolean, + onShowTaskOption: (task: Task) -> Unit +) { + Scaffold( + topBar = { + BloomTopAppBar( + hasBackNavigation = true, + navigationIcon = { + IconButton(onClick = onClickNavigateBack) { + Icon( + imageVector = Icons.Outlined.ArrowBack, + contentDescription = "Back" + ) + } + } + ) { + Text(text = "Tasks History") + } + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp) + ) { + tasks.forEach { (date, tasks) -> + stickyHeader { + Text( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(bottom = 4.dp), + text = date.prettyFormat(), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + ) + } + items(tasks) { + HistoryCard( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + task = it, + hourFormat = timeFormat, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime, + onClickDelete = onClickDelete, + onClickCancel = onClickCancel, + showTaskOption = showTaskOption, + onShowTaskOption = onShowTaskOption + ) + } + } + } + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/StatisticsScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/StatisticsScreen.kt new file mode 100644 index 0000000..09ba702 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/StatisticsScreen.kt @@ -0,0 +1,534 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.statistics + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardDoubleArrowLeft +import androidx.compose.material.icons.filled.KeyboardDoubleArrowRight +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.navigator.currentOrThrow +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar +import com.joelkanyi.focusbloom.core.utils.LocalAppNavigator +import com.joelkanyi.focusbloom.core.utils.calculateEndTime +import com.joelkanyi.focusbloom.core.utils.completedTasks +import com.joelkanyi.focusbloom.core.utils.durationInMinutes +import com.joelkanyi.focusbloom.core.utils.getLast52Weeks +import com.joelkanyi.focusbloom.core.utils.prettyFormat +import com.joelkanyi.focusbloom.core.utils.prettyTimeDifference +import com.joelkanyi.focusbloom.core.utils.taskColor +import com.joelkanyi.focusbloom.core.utils.taskIcon +import com.joelkanyi.focusbloom.feature.statistics.component.BarChart +import com.joelkanyi.focusbloom.feature.statistics.component.TickPositionState +import com.joelkanyi.focusbloom.platform.StatusBarColors +import io.github.koalaplot.core.ChartLayout +import io.github.koalaplot.core.util.ExperimentalKoalaPlotApi +import io.github.koalaplot.core.xychart.TickPosition +import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDate +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource +import org.koin.compose.rememberKoinInject + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun StatisticsScreen() { + val screenModel: StatisticsScreenModel = rememberKoinInject() + + StatusBarColors( + statusBarColor = MaterialTheme.colorScheme.background, + navBarColor = MaterialTheme.colorScheme.background + ) + val navigator = LocalAppNavigator.currentOrThrow + val tasksHistory = screenModel.tasks.collectAsState().value + val lastFiftyTwoWeeks = getLast52Weeks().asReversed() + val hourFormat = screenModel.hourFormat.collectAsState().value ?: 24 + val sessionTime = screenModel.sessionTime.collectAsState().value ?: 25 + val shortBreakTime = screenModel.shortBreakTime.collectAsState().value ?: 5 + val longBreakTime = screenModel.longBreakTime.collectAsState().value ?: 15 + val coroutineScope = rememberCoroutineScope() + val pagerState = rememberPagerState( + initialPage = lastFiftyTwoWeeks.size - 1, + initialPageOffsetFraction = 0f, + pageCount = { + lastFiftyTwoWeeks.size + } + ) + val selectedWeek = lastFiftyTwoWeeks[pagerState.currentPage].first + val selectedWeekTasks = tasksHistory.completedTasks( + lastFiftyTwoWeeks[pagerState.currentPage].second + ).map { it.toFloat() } + val tickPositionState by remember { + mutableStateOf( + TickPositionState( + TickPosition.Outside, + TickPosition.Outside + ) + ) + } + + StatisticsScreenContent( + hourFormat = hourFormat, + sessionTime = sessionTime, + tickPositionState = tickPositionState, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime, + pagerState = pagerState, + selectedWeek = selectedWeek, + selectedWeekTasks = selectedWeekTasks, + tasksHistory = tasksHistory.groupBy { it.date.date }, + onClickSeeAllTasks = { + navigator.push(AllStatisticsScreen()) + }, + onClickThisWeek = { + coroutineScope.launch { + pagerState.animateScrollToPage(lastFiftyTwoWeeks.size - 1) + } + }, + onClickNextWeek = { + coroutineScope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + }, + onClickPreviousWeek = { + coroutineScope.launch { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } + }, + onClickDelete = { + screenModel.deleteTask(it) + }, + showTaskOption = { + screenModel.openedTasks.contains(it) + }, + onShowTaskOption = { + screenModel.openTaskOptions(it) + }, + onClickCancel = { + screenModel.openTaskOptions(it) + } + ) +} + +@OptIn( + ExperimentalKoalaPlotApi::class, + ExperimentalMaterial3Api::class, + ExperimentalFoundationApi::class, + ExperimentalResourceApi::class +) +@Composable +fun StatisticsScreenContent( + selectedWeek: String, + hourFormat: Int, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int, + selectedWeekTasks: List, + tickPositionState: TickPositionState, + tasksHistory: Map>, + onClickSeeAllTasks: () -> Unit, + pagerState: PagerState, + onClickNextWeek: () -> Unit, + onClickPreviousWeek: () -> Unit, + onClickThisWeek: () -> Unit, + onClickDelete: (task: Task) -> Unit, + onClickCancel: (task: Task) -> Unit, + showTaskOption: (task: Task) -> Boolean, + onShowTaskOption: (task: Task) -> Unit +) { + Scaffold( + topBar = { + BloomTopAppBar( + hasBackNavigation = false, + title = { + Text( + modifier = Modifier.fillMaxWidth(.7f), + text = "Your Statistics", + style = MaterialTheme.typography.displaySmall.copy( + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + ) + }, + actions = { + AnimatedVisibility(selectedWeek != "This Week") { + TextButton( + onClick = onClickThisWeek + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + modifier = Modifier.size(18.dp), + painter = painterResource("redo.xml"), + contentDescription = "This Week" + ) + Text( + text = "This Week", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline + ) + ) + } + } + } + } + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier.padding(paddingValues), + contentPadding = PaddingValues(horizontal = 16.dp) + ) { + item { + WeeksController( + onClickPreviousWeek = onClickPreviousWeek, + selectedWeek = selectedWeek, + onClickNextWeek = onClickNextWeek + ) + } + item { + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + ) { + ChartLayout( + modifier = Modifier + .fillMaxWidth() + .sizeIn(maxHeight = 300.dp) + ) { + BarChart( + tickPositionState = tickPositionState, + entries = selectedWeekTasks + ) + } + } + } + + item { + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Your History", + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold + ) + ) + if (tasksHistory.values.size > 3) { + TextButton(onClick = onClickSeeAllTasks) { + Text( + text = "See All", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + fontSize = 16.sp + ) + ) + } + } + } + } + + tasksHistory.forEach { (date, tasks) -> + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp, bottom = 6.dp), + text = date.prettyFormat(), + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + textAlign = TextAlign.End + ) + ) + } + items(tasks) { + HistoryCard( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 6.dp), + task = it, + hourFormat = hourFormat, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime, + onClickDelete = onClickDelete, + onClickCancel = onClickCancel, + showTaskOption = showTaskOption, + onShowTaskOption = onShowTaskOption + ) + } + } + } + } +} + +@Composable +private fun WeeksController( + onClickPreviousWeek: () -> Unit, + selectedWeek: String, + onClickNextWeek: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + modifier = Modifier.size(24.dp), + onClick = onClickPreviousWeek + ) { + Icon( + imageVector = Icons.Default.KeyboardDoubleArrowLeft, + contentDescription = "Previous Week" + ) + } + Text( + text = selectedWeek, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.SemiBold + ) + ) + IconButton( + modifier = Modifier.size(24.dp), + onClick = { + if (selectedWeek != "This Week") { + onClickNextWeek() + } + } + ) { + Icon( + imageVector = Icons.Default.KeyboardDoubleArrowRight, + contentDescription = "Next Week", + tint = if (selectedWeek != "This Week") { + MaterialTheme.colorScheme.onBackground + } else { + MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f) + } + ) + } + } +} + +@OptIn(ExperimentalResourceApi::class) +@Composable +fun HistoryCard( + task: Task, + modifier: Modifier = Modifier, + hourFormat: Int, + sessionTime: Int, + shortBreakTime: Int, + longBreakTime: Int, + onClickCancel: (task: Task) -> Unit, + onClickDelete: (task: Task) -> Unit, + showTaskOption: (task: Task) -> Boolean, + onShowTaskOption: (task: Task) -> Unit +) { + Column { + Card( + modifier = modifier + ) { + Row( + modifier = Modifier.padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(48.dp) + .clip(MaterialTheme.shapes.large) + .background( + color = Color(task.type.taskColor()), + shape = MaterialTheme.shapes.medium + ), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier + .padding(12.dp) + .size(24.dp), + painter = painterResource(task.type.taskIcon()), + contentDescription = "Task Icon", + tint = MaterialTheme.colorScheme.onPrimary + ) + } + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.fillMaxWidth(.8f), + text = task.name, + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp + ), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Icon( + modifier = Modifier + .size(18.dp) + .clickable { + onShowTaskOption(task) + }, + imageVector = Icons.Default.MoreVert, + contentDescription = "More Options" + ) + } + if (task.description != null) { + Text( + text = task.description, + style = MaterialTheme.typography.bodyMedium, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "${ + task.durationInMinutes( + focusSessions = task.focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ) + } minutes", + style = MaterialTheme.typography.displaySmall.copy( + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold + ) + ) + Text( + prettyTimeDifference( + start = task.start, + end = task.start.calculateEndTime( + focusSessions = task.focusSessions, + sessionTime = sessionTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime + ), + timeFormat = hourFormat + ), + style = MaterialTheme.typography.displaySmall.copy( + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold + ) + ) + } + } + } + AnimatedVisibility(visible = showTaskOption(task)) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton(onClick = { + onClickCancel(task) + }) { + Text( + text = "Cancel", + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.SemiBold + ) + ) + } + Spacer(modifier = Modifier.width(16.dp)) + TextButton(onClick = { + onClickDelete(task) + }) { + Text( + text = "Delete", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.ExtraBold + ) + ) + } + } + } + } + } + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/StatisticsScreenModel.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/StatisticsScreenModel.kt new file mode 100644 index 0000000..74dde16 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/StatisticsScreenModel.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.statistics + +import androidx.compose.runtime.mutableStateListOf +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.domain.repository.settings.SettingsRepository +import com.joelkanyi.focusbloom.core.domain.repository.tasks.TasksRepository +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class StatisticsScreenModel( + private val tasksRepository: TasksRepository, + settingsRepository: SettingsRepository +) : ScreenModel { + val hourFormat = settingsRepository.getHourFormat() + .map { + it + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + val sessionTime = settingsRepository.getSessionTime() + .map { + it + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + val shortBreakTime = settingsRepository.getShortBreakTime() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + val longBreakTime = settingsRepository.getLongBreakTime() + .map { it } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + val tasks = tasksRepository.getTasks() + .map { tasks -> + tasks.sortedByDescending { it.date } + .filter { + it.completed + } + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList() + ) + + fun deleteTask(task: Task) { + coroutineScope.launch { + tasksRepository.deleteTask(task.id) + } + } + + val openedTasks = mutableStateListOf(null) + fun openTaskOptions(task: Task?) { + if (openedTasks.contains(task)) { + openedTasks.remove(task) + } else { + openedTasks.add(task) + } + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/component/StatisticsChart.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/component/StatisticsChart.kt new file mode 100644 index 0000000..7d1c9f8 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/statistics/component/StatisticsChart.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.statistics.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.absolutePadding +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.joelkanyi.focusbloom.core.utils.aAllEntriesAreZero +import io.github.koalaplot.core.ChartLayout +import io.github.koalaplot.core.bar.BarChartEntry +import io.github.koalaplot.core.bar.DefaultBarChartEntry +import io.github.koalaplot.core.bar.DefaultVerticalBar +import io.github.koalaplot.core.bar.VerticalBarChart +import io.github.koalaplot.core.util.ExperimentalKoalaPlotApi +import io.github.koalaplot.core.util.VerticalRotation +import io.github.koalaplot.core.util.rotateVertically +import io.github.koalaplot.core.util.toString +import io.github.koalaplot.core.xychart.LinearAxisModel +import io.github.koalaplot.core.xychart.TickPosition +import io.github.koalaplot.core.xychart.XYChart +import io.github.koalaplot.core.xychart.rememberAxisStyle + +internal val padding = 8.dp +internal val paddingMod = Modifier.padding(padding) + +private const val BarWidth = 0.8f + +@Composable +fun AxisTitle(title: String, modifier: Modifier = Modifier) { + Text( + title, + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.titleMedium, + modifier = modifier + ) +} + +@Composable +fun AxisLabel(label: String, modifier: Modifier = Modifier) { + Text( + label, + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.bodySmall, + modifier = modifier, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) +} + +fun barChartEntries(fibonacci: List): List> { + return buildList { + fibonacci.forEachIndexed { index, fl -> + add( + DefaultBarChartEntry( + xValue = (index + 1).toFloat(), + yMin = 0f, + yMax = fl + ) + ) + } + } +} + +@OptIn(ExperimentalKoalaPlotApi::class) +@Composable +fun BarChart(tickPositionState: TickPositionState, entries: List) { + val yAxisRange = 0f..if (entries.aAllEntriesAreZero()) 20f else entries.maxOf { it } + val xAxisRange = 0.5f..7.5f + ChartLayout( + modifier = paddingMod, + title = { } + ) { + XYChart( + xAxisModel = LinearAxisModel( + xAxisRange, + minimumMajorTickIncrement = 1f, + minimumMajorTickSpacing = 10.dp, + zoomRangeLimit = 3f, + minorTickCount = 0 + ), + yAxisModel = LinearAxisModel( + yAxisRange, + minimumMajorTickIncrement = 1f, + minorTickCount = 0 + ), + xAxisStyle = rememberAxisStyle( + tickPosition = tickPositionState.horizontalAxis, + color = Color.LightGray + ), + xAxisLabels = { + AxisLabel( + when (it) { + 1f -> "Mon" + 2f -> "Tue" + 3f -> "Wed" + 4f -> "Thu" + 5f -> "Fri" + 6f -> "Sat" + 7f -> "Sun" + else -> "" + }, + Modifier.padding(top = 2.dp) + ) + }, + xAxisTitle = { + AxisTitle( + "Day of the Week", + modifier = Modifier.padding(top = 8.dp) + ) + }, + yAxisStyle = rememberAxisStyle(tickPosition = tickPositionState.verticalAxis), + yAxisLabels = { + AxisLabel(it.toString(0), Modifier.absolutePadding(right = 2.dp)) + }, + yAxisTitle = { + AxisTitle( + "Tasks Completed", + modifier = Modifier.rotateVertically(VerticalRotation.COUNTER_CLOCKWISE) + .padding(bottom = padding) + ) + }, + verticalMajorGridLineStyle = null + ) { + VerticalBarChart( + series = listOf( + barChartEntries( + fibonacci = entries + ) + ), + bar = { _, _, value -> + DefaultVerticalBar( + brush = SolidColor(MaterialTheme.colorScheme.primary), + modifier = Modifier.fillMaxWidth(BarWidth) + ) { + HoverSurface { Text(value.yMax.toString()) } + } + } + ) + } + } +} + +data class TickPositionState( + val verticalAxis: TickPosition, + val horizontalAxis: TickPosition +) + +@Composable +fun HoverSurface(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Surface( + shadowElevation = 2.dp, + shape = MaterialTheme.shapes.medium, + color = Color.LightGray, + modifier = modifier.padding(padding) + ) { + Box(modifier = Modifier.padding(padding)) { + content() + } + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/taskprogress/TaskProgressScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/taskprogress/TaskProgressScreen.kt new file mode 100644 index 0000000..02aef2f --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/taskprogress/TaskProgressScreen.kt @@ -0,0 +1,437 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.taskprogress + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.joelkanyi.focusbloom.core.domain.model.SessionType +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.presentation.component.BloomTimerControls +import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar +import com.joelkanyi.focusbloom.core.presentation.component.TaskProgress +import com.joelkanyi.focusbloom.core.presentation.theme.LongBreakColor +import com.joelkanyi.focusbloom.core.presentation.theme.SessionColor +import com.joelkanyi.focusbloom.core.presentation.theme.ShortBreakColor +import com.joelkanyi.focusbloom.core.utils.UiEvents +import com.joelkanyi.focusbloom.core.utils.durationInMinutes +import com.joelkanyi.focusbloom.core.utils.sessionType +import com.joelkanyi.focusbloom.core.utils.toMillis +import com.joelkanyi.focusbloom.core.utils.toMinutes +import com.joelkanyi.focusbloom.core.utils.toPercentage +import com.joelkanyi.focusbloom.core.utils.toTimer +import com.joelkanyi.focusbloom.platform.StatusBarColors +import kotlinx.coroutines.flow.collectLatest +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +data class TaskProgressScreen( + val taskId: Int +) : Screen, KoinComponent { + + @Composable + override fun Content() { + val screenModel = get() + val snackbarHostState = remember { SnackbarHostState() } + val task = screenModel.task.collectAsState().value + val navigator = LocalNavigator.currentOrThrow + val timer = Timer.tickingTime.collectAsState().value + val timerState = Timer.timerState.collectAsState().value + val shortBreakColor = screenModel.shortBreakColor.collectAsState().value + val longBreakColor = screenModel.longBreakColor.collectAsState().value + val focusColor = screenModel.focusColor.collectAsState().value + val focusTime = screenModel.focusTime.collectAsState().value ?: (25).toMillis() + val shortBreakTime = screenModel.shortBreakTime.collectAsState().value ?: (5).toMillis() + val longBreakTime = screenModel.longBreakTime.collectAsState().value ?: (15).toMillis() + + val containerColor = when (task?.current.sessionType()) { + SessionType.Focus -> { + if (focusColor == null || focusColor == 0L) { + Color(SessionColor) + } else { + Color( + focusColor + ) + } + } + + SessionType.LongBreak -> { + if (longBreakColor == null || longBreakColor == 0L) { + Color(LongBreakColor) + } else { + Color( + longBreakColor + ) + } + } + + SessionType.ShortBreak -> { + if (shortBreakColor == null || shortBreakColor == 0L) { + Color(ShortBreakColor) + } else { + Color( + shortBreakColor + ) + } + } + } + StatusBarColors( + statusBarColor = containerColor, + navBarColor = containerColor + ) + LaunchedEffect(key1 = Unit) { + screenModel.getTask(taskId) + Timer.eventsFlow.collectLatest { event -> + when (event) { + is UiEvents.ShowSnackbar -> { + snackbarHostState.showSnackbar(event.message) + } + + else -> {} + } + } + } + + if (task?.completed == true) { + SuccessfulCompletionOfTask( + title = "Task Completed", + message = "You have successfully completed this task", + onConfirm = { + Timer.reset() + navigator.pop() + } + ) + } + + FocusTimeScreenContent( + task = task, + focusTime = focusTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime, + timerValue = timer, + snackbarHostState = snackbarHostState, + timerState = timerState, + containerColor = containerColor, + onClickNavigateBack = { + navigator.pop() + }, + onClickNext = { + screenModel.moveToNextSessionOfTheTask() + }, + onClickReset = { + screenModel.resetCurrentSessionOfTheTask() + }, + onClickAction = { state -> + when (state) { + TimerState.Ticking -> { + Timer.pause() + } + + TimerState.Paused -> { + Timer.resume() + } + + TimerState.Stopped -> { + // screenModel.setTime(task?.focusTime ?: 20) + } + + TimerState.Idle -> { + Timer.start( + update = { + screenModel.updateConsumedTime() + }, + executeTasks = { + screenModel.executeTasks() + } + ) + screenModel.resetAllTasksToInactive() + screenModel.updateActiveTask(taskId, true) + } + + TimerState.Finished -> { + // screenModel.setTime(task?.focusTime ?: 20) + } + } + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FocusTimeScreenContent( + focusTime: Long, + shortBreakTime: Long, + longBreakTime: Long, + containerColor: Color, + timerValue: Long, + timerState: TimerState, + task: Task?, + snackbarHostState: SnackbarHostState, + onClickNavigateBack: () -> Unit, + onClickAction: (state: TimerState) -> Unit, + onClickNext: () -> Unit, + onClickReset: () -> Unit +) { + Scaffold( + containerColor = containerColor, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + topBar = { + BloomTopAppBar( + hasBackNavigation = true, + navigationIcon = { + IconButton(onClick = onClickNavigateBack) { + Icon( + imageVector = Icons.Outlined.ArrowBack, + contentDescription = "Add Task Back Button", + tint = MaterialTheme.colorScheme.onPrimary + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = containerColor + ) + ) + } + ) { paddingValues -> + Box( + modifier = Modifier.padding(paddingValues).fillMaxSize() + ) { + if (task == null) { + Text( + modifier = Modifier.align(Alignment.Center), + text = "Task not found" + ) + } else { + LazyColumn( + modifier = Modifier.padding(PaddingValues(horizontal = 16.dp)) + ) { + item { + Card( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier.fillMaxWidth(.85f), + text = task.name, + style = MaterialTheme.typography.titleSmall, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = buildAnnotatedString { + withStyle( + style = SpanStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp + ) + ) { + append("${task.currentCycle}") + } + append("/${task.focusSessions}") + } + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Total: ${ + task.durationInMinutes( + sessionTime = focusTime.toMinutes(), + shortBreakTime = shortBreakTime.toMinutes(), + longBreakTime = longBreakTime.toMinutes(), + focusSessions = task.focusSessions + ) + } minutes", + style = MaterialTheme.typography.bodySmall + ) + Text( + text = when (task.current.sessionType()) { + SessionType.Focus -> "${focusTime.toMinutes()} min" + SessionType.ShortBreak -> "${shortBreakTime.toMinutes()} min" + SessionType.LongBreak -> "${longBreakTime.toMinutes()} min" + } + ) + } + } + } + } + + item { + Spacer(modifier = Modifier.height(32.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + TaskProgress( + percentage = timerValue.toPercentage( + when (task.current.sessionType()) { + SessionType.Focus -> focusTime + SessionType.ShortBreak -> shortBreakTime + SessionType.LongBreak -> longBreakTime + } + ), + radius = 40.dp, + content = timerValue.toTimer(), + mainColor = MaterialTheme.colorScheme.primary, + counterColor = MaterialTheme.colorScheme.onPrimary + ) + } + } + + item { + Spacer(modifier = Modifier.height(48.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = when (task.current.sessionType()) { + SessionType.Focus -> "Focus Time" + SessionType.ShortBreak -> "Short Break" + SessionType.LongBreak -> "Long Break" + }, + style = MaterialTheme.typography.displaySmall.copy( + color = MaterialTheme.colorScheme.onPrimary + ), + textAlign = TextAlign.Center + ) + } + + item { + Spacer(modifier = Modifier.height(56.dp)) + BloomTimerControls( + modifier = Modifier.fillMaxWidth(), + state = timerState, + onClickReset = onClickReset, + onClickNext = onClickNext, + onClickAction = onClickAction + ) + } + } + } + } + } +} + +@OptIn(ExperimentalResourceApi::class) +@Composable +fun SuccessfulCompletionOfTask( + modifier: Modifier = Modifier, + title: String, + message: String, + onConfirm: () -> Unit +) { + AlertDialog( + modifier = modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + icon = { + Image( + modifier = Modifier.size(48.dp), + painter = painterResource("ic_complete.xml"), + contentDescription = "Task Completed" + ) + }, + containerColor = MaterialTheme.colorScheme.background, + onDismissRequest = onConfirm, + title = { + Text( + modifier = Modifier.fillMaxWidth(), + text = title, + style = MaterialTheme.typography.titleMedium.copy( + textAlign = TextAlign.Center + ) + ) + }, + text = { + Text( + modifier = Modifier.fillMaxWidth(), + text = message, + style = MaterialTheme.typography.bodyMedium.copy( + textAlign = TextAlign.Center + ) + ) + }, + dismissButton = {}, + confirmButton = { + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onConfirm + ) { + Text( + text = "OK", + style = MaterialTheme.typography.titleSmall + ) + } + } + ) +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/taskprogress/TaskProgressScreenModel.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/taskprogress/TaskProgressScreenModel.kt new file mode 100644 index 0000000..536ede1 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/taskprogress/TaskProgressScreenModel.kt @@ -0,0 +1,390 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.taskprogress + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import com.joelkanyi.focusbloom.core.domain.model.SessionType +import com.joelkanyi.focusbloom.core.domain.model.Task +import com.joelkanyi.focusbloom.core.domain.repository.settings.SettingsRepository +import com.joelkanyi.focusbloom.core.domain.repository.tasks.TasksRepository +import com.joelkanyi.focusbloom.core.utils.sessionType +import com.joelkanyi.focusbloom.core.utils.toMillis +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class TaskProgressScreenModel( + settingsRepository: SettingsRepository, + private val tasksRepository: TasksRepository +) : ScreenModel { + val shortBreakColor = settingsRepository.shortBreakColor() + .map { it } + .stateIn( + coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + val longBreakColor = settingsRepository.longBreakColor() + .map { it } + .stateIn( + coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + val focusColor = settingsRepository.focusColor() + .map { it } + .stateIn( + coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + val focusTime = settingsRepository.getSessionTime() + .map { + it?.toMillis() ?: (25).toMillis() + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + val shortBreakTime = settingsRepository.getShortBreakTime() + .map { + it?.toMillis() ?: (5).toMillis() + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + val longBreakTime = settingsRepository.getLongBreakTime() + .map { it?.toMillis() ?: (15).toMillis() } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + private val _task = MutableStateFlow(null) + val task = _task.asStateFlow() + fun getTask(taskId: Int) { + coroutineScope.launch { + tasksRepository.getTask(taskId).collectLatest { + _task.value = it + } + } + } + + /** + * This function updates the consumed focus time of the task + * @param taskId the id of the task + * @param consumedTime the consumed time of the focus + */ + private fun updateConsumedFocusTime(taskId: Int, consumedTime: Long) { + coroutineScope.launch { + tasksRepository.updateConsumedFocusTime(taskId, consumedTime) + } + } + + /** + * This function updates the consumed short break time of the task + * @param taskId the id of the task + * @param consumedTime the consumed time of the short break + */ + private fun updateConsumedShortBreakTime(taskId: Int, consumedTime: Long) { + coroutineScope.launch { + tasksRepository.updateConsumedShortBreakTime(taskId, consumedTime) + } + } + + /** + * This function updates the consumed long break time of the task + * @param taskId the id of the task + * @param consumedTime the consumed time of the long break + */ + private fun updateConsumedLongBreakTime(taskId: Int, consumedTime: Long) { + coroutineScope.launch { + tasksRepository.updateConsumedLongBreakTime(taskId, consumedTime) + } + } + + /** + * This function updates task as either in progress or not in progress + * @param taskId the id of the task + * @param inProgressTask the in progress task + */ + private fun updateInProgressTask(taskId: Int, inProgressTask: Boolean) { + coroutineScope.launch { + tasksRepository.updateTaskInProgress(taskId, inProgressTask) + } + } + + fun updateActiveTask(taskId: Int, activeTask: Boolean) { + coroutineScope.launch { + tasksRepository.updateTaskActive(id = taskId, active = activeTask) + } + } + + /** + * This function updates task as either completed or not completed + * @param taskId the id of the task + * @param completedTask the completed task + */ + private fun updateCompletedTask(taskId: Int, completedTask: Boolean) { + coroutineScope.launch { + tasksRepository.updateTaskCompleted(taskId, completedTask) + } + } + + fun resetAllTasksToInactive() { + coroutineScope.launch { + tasksRepository.updateAllTasksActiveStatusToInactive() + } + } + + /** + * This function updates the current cycle of the task + * @param taskId the id of the task + * @param currentCycle the current cycle of the task + */ + private fun updateCurrentCycle(taskId: Int, currentCycle: Int) { + coroutineScope.launch { + tasksRepository.updateTaskCycleNumber(taskId, currentCycle) + } + } + + /** + * This function updates the current session of the task (Focus, ShortBreak, LongBreak) + * @param taskId the id of the task + * @param currentSession the current session of the task + */ + private fun updateCurrentSession(taskId: Int, currentSession: String) { + coroutineScope.launch { + tasksRepository.updateCurrentSessionName(taskId, currentSession) + } + } + + fun updateConsumedTime() { + when (task.value?.current) { + "Focus" -> updateConsumedFocusTime( + task.value?.id ?: -1, + Timer.tickingTime.value + ) + + "ShortBreak" -> updateConsumedShortBreakTime( + task.value?.id ?: -1, + Timer.tickingTime.value + ) + + "LongBreak" -> updateConsumedLongBreakTime( + task.value?.id ?: -1, + Timer.tickingTime.value + ) + } + } + + fun executeTasks() { + coroutineScope.launch { + if (task.value?.currentCycle?.equals(0) == true) { + println("executeTasks: first cycle") + updateCurrentCycle(task.value?.id ?: 0, 1) + updateCurrentSession(task.value?.id ?: 0, "Focus") + updateInProgressTask(task.value?.id ?: 0, true) + Timer.setTickingTime(focusTime.value ?: 0L) + Timer.start( + update = { + updateConsumedTime() + }, + executeTasks = { + executeTasks() + } + ) + } else { + when (task.value?.current) { + "Focus" -> { + if (task.value?.currentCycle == task.value?.focusSessions) { + println("executeTasks: going for a long break after a focus session") + updateCurrentSession(task.value?.id ?: 0, "LongBreak") + updateInProgressTask(task.value?.id ?: 0, true) + Timer.setTickingTime(longBreakTime.value ?: 0L) + Timer.start( + update = { + updateConsumedTime() + }, + executeTasks = { + executeTasks() + } + ) + } else { + println("executeTasks: going for a short break after a focus session") + updateCurrentSession(task.value?.id ?: 0, "ShortBreak") + updateInProgressTask(task.value?.id ?: 0, true) + Timer.setTickingTime(shortBreakTime.value ?: 0L) + Timer.start( + update = { + updateConsumedTime() + }, + executeTasks = { + executeTasks() + } + ) + } + } + + "ShortBreak" -> { + println("executeTasks: going for a focus session after a short break") + updateCurrentSession(task.value?.id ?: 0, "Focus") + updateCurrentCycle( + task.value?.id ?: 0, + task.value?.currentCycle?.plus(1) ?: (0 + 1) + ) + updateInProgressTask(task.value?.id ?: 0, true) + Timer.setTickingTime(focusTime.value ?: 0L) + Timer.start( + update = { + updateConsumedTime() + }, + executeTasks = { + executeTasks() + } + ) + } + + "LongBreak" -> { + println("executeTasks: completed all cycles") + val taskId = task.value?.id ?: 0 + updateInProgressTask(taskId, false) + updateCompletedTask(taskId, true) + updateActiveTask(taskId, false) + Timer.stop() + Timer.reset() + } + } + } + } + } + + fun moveToNextSessionOfTheTask() { + coroutineScope.launch { + when (task.value?.current.sessionType()) { + SessionType.Focus -> { + if (task.value?.currentCycle == task.value?.focusSessions) { + updateCurrentSession(task.value?.id ?: 0, "LongBreak") + updateInProgressTask(task.value?.id ?: 0, true) + Timer.setTickingTime(longBreakTime.value ?: 0L) + Timer.start( + update = { + updateConsumedTime() + }, + executeTasks = { + executeTasks() + } + ) + } else { + updateCurrentSession(task.value?.id ?: 0, "ShortBreak") + updateInProgressTask(task.value?.id ?: 0, true) + Timer.setTickingTime(shortBreakTime.value ?: 0L) + Timer.start( + update = { + updateConsumedTime() + }, + executeTasks = { + executeTasks() + } + ) + } + } + + SessionType.LongBreak -> { + val taskId = task.value?.id ?: 0 + updateInProgressTask(taskId, false) + updateCompletedTask(taskId, true) + updateActiveTask(taskId, false) + Timer.stop() + Timer.reset() + } + + SessionType.ShortBreak -> { + updateCurrentSession(task.value?.id ?: 0, "Focus") + updateCurrentCycle( + task.value?.id ?: 0, + task.value?.currentCycle?.plus(1) ?: (0 + 1) + ) + updateInProgressTask(task.value?.id ?: 0, true) + Timer.setTickingTime(focusTime.value ?: 0L) + Timer.start( + update = { + updateConsumedTime() + }, + executeTasks = { + executeTasks() + } + ) + } + } + } + } + + fun resetCurrentSessionOfTheTask() { + coroutineScope.launch { + when (task.value?.current.sessionType()) { + SessionType.Focus -> { + updateCurrentSession(task.value?.id ?: 0, "Focus") + updateInProgressTask(task.value?.id ?: 0, true) + Timer.setTickingTime(focusTime.value ?: 0L) + Timer.start( + update = { + updateConsumedTime() + }, + executeTasks = { + executeTasks() + } + ) + } + + SessionType.LongBreak -> { + val taskId = task.value?.id ?: 0 + updateInProgressTask(taskId, false) + updateCompletedTask(taskId, true) + updateActiveTask(taskId, false) + Timer.stop() + Timer.reset() + } + + SessionType.ShortBreak -> { + updateCurrentSession(task.value?.id ?: 0, "ShortBreak") + updateInProgressTask(task.value?.id ?: 0, true) + Timer.setTickingTime(shortBreakTime.value ?: 0L) + Timer.start( + update = { + updateConsumedTime() + }, + executeTasks = { + executeTasks() + } + ) + } + } + } + } +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/taskprogress/Timer.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/taskprogress/Timer.kt new file mode 100644 index 0000000..8b554bc --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/feature/taskprogress/Timer.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.feature.taskprogress + +import com.joelkanyi.focusbloom.core.utils.UiEvents +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +object Timer { + private val _eventsFlow = MutableSharedFlow() + val eventsFlow = _eventsFlow.asSharedFlow() + + private var job: Job? = null + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + private val _tickingTime = MutableStateFlow(0L) + val tickingTime: StateFlow = _tickingTime.asStateFlow() + fun setTickingTime(time: Long) { + _tickingTime.value = time + } + + private val _timerState = MutableStateFlow(TimerState.Idle) + val timerState: StateFlow = _timerState.asStateFlow() + private fun setTimerState(state: TimerState) { + _timerState.value = state + } + + private val _timerEvent = MutableStateFlow(TimerEvent.Idle) + private val timerEvent: StateFlow = _timerEvent.asStateFlow() + + fun stop() { + job?.cancel() + _timerState.value = TimerState.Stopped + } + + private fun finish(executeTasks: () -> Unit) { + scope.launch { + stop() + _timerState.emit(TimerState.Finished) + _timerEvent.emit(TimerEvent.TimerEventFinished) + if (timerEvent.value == TimerEvent.TimerEventFinished) { + executeTasks() + } + } + } + + fun reset() { + scope.launch { + stop() + _timerState.emit(TimerState.Idle) + _timerEvent.emit(TimerEvent.TimerEventStarted) + } + } + + fun pause() { + scope.launch { + _timerState.emit(TimerState.Paused) + } + } + + fun resume() { + scope.launch { + _timerState.emit(TimerState.Ticking) + } + } + + fun start(update: () -> Unit, executeTasks: () -> Unit) { + job?.cancel() + job = scope.launch { + setTimerState(TimerState.Ticking) + _timerEvent.emit(TimerEvent.TimerEventStarted) + + while (tickingTime.value > 0L) { + delay(200L) + + if (timerState.value == TimerState.Ticking) { + setTickingTime(tickingTime.value - 200L) + update() + } + } + + finish(executeTasks) + } + } +} + +sealed class TimerState { + data object Idle : TimerState() // timer ready to start + data object Ticking : TimerState() // timer is ticking + data object Paused : TimerState() // timer in paused state + data object Finished : TimerState() // timer finished + data object Stopped : TimerState() // timer stopped programmatically +} + +sealed class TimerEvent { + data object TimerEventStarted : TimerEvent() + data object TimerEventFinished : TimerEvent() + data object Idle : TimerEvent() +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/home/AllTasksScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/home/AllTasksScreen.kt deleted file mode 100644 index fac599d..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/home/AllTasksScreen.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.joelkanyi.focusbloom.home - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ArrowBack -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar -import com.joelkanyi.focusbloom.core.presentation.component.TaskCard -import com.joelkanyi.focusbloom.core.samples.sampleTasks - -class AllTasksScreen : Screen { - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - AllTasksScreenContent( - onClickNavigateBack = { - navigator.pop() - }, - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AllTasksScreenContent( - onClickNavigateBack: () -> Unit = {}, -) { - Scaffold( - topBar = { - BloomTopAppBar( - hasBackNavigation = true, - navigationIcon = { - IconButton(onClick = onClickNavigateBack) { - Icon( - imageVector = Icons.Outlined.ArrowBack, - contentDescription = "Back", - ) - } - }, - ) { - Text(text = "Today's Tasks (${sampleTasks.size})") - } - }, - ) { paddingValues -> - Box( - modifier = Modifier - .padding(paddingValues) - .fillMaxSize(), - ) { - LazyColumn( - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - items(sampleTasks) { - TaskCard( - task = it, - onClick = { }, - ) - } - } - } - } -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/home/HomeScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/home/HomeScreen.kt deleted file mode 100644 index 8958be0..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/home/HomeScreen.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.joelkanyi.focusbloom.home - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Card -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -// import com.joelkanyi.focusbloom.presentation.component.TaskCard -// import com.joelkanyi.focusbloom.presentation.component.TaskProgress -import com.joelkanyi.focusbloom.domain.model.Task -import com.joelkanyi.focusbloom.core.presentation.component.TaskCard -import com.joelkanyi.focusbloom.core.presentation.component.TaskProgress -import com.joelkanyi.focusbloom.taskprogress.FocusTimeScreen -import com.joelkanyi.focusbloom.core.samples.sampleTasks - -// import com.joelkanyi.samples.sampleTasks - -class HomeScreen : Screen { - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - HomeScreenContent( - onClickTask = { - navigator.push(FocusTimeScreen(taskId = it.id)) - // navigator.navigate(FocusTimeScreenDestination(taskId = it.id)) - }, - onClickSeeAllTasks = { - navigator.push(AllTasksScreen()) - }, - ) - } -} - -@Composable -private fun HomeScreenContent( - onClickTask: (task: Task) -> Unit = {}, - onClickSeeAllTasks: () -> Unit = {}, -) { - Scaffold { paddingValues -> - Box( - modifier = Modifier - .padding(paddingValues) - .fillMaxSize(), - ) { - LazyColumn( - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - item { - Text( - text = "Hello, Joel", - style = MaterialTheme.typography.displaySmall, - ) - } - item { - Card { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - TaskProgress( - mainColor = MaterialTheme.colorScheme.secondary, - percentage = 75f, - ) - Column( - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - Text( - text = "Wow!, Your daily tasks are almost done", - style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.SemiBold, - ), - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "12 of 16 tasks completed", - style = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onSurface, - ), - ) - } - } - } - } - item { - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = "Today's Tasks (${sampleTasks.size})", - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold, - ), - ) - TextButton(onClick = onClickSeeAllTasks) { - Text( - text = "See All", - style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary, - ), - ) - } - } - } - items(sampleTasks.take(4)) { - TaskCard( - task = it, - onClick = onClickTask, - ) - } - } - } - } -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/main/MainScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/main/MainScreen.kt new file mode 100644 index 0000000..ed45e21 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/main/MainScreen.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.main + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.BottomNavigation +import androidx.compose.material.BottomNavigationItem +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.CurrentScreen +import cafe.adriel.voyager.navigator.tab.LocalTabNavigator +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabNavigator +import com.joelkanyi.focusbloom.core.presentation.component.BloomNavigationRailBar +import com.joelkanyi.focusbloom.core.presentation.component.BloomTab +import com.joelkanyi.focusbloom.core.presentation.utils.FilledIcon + +class MainScreen : Screen { + + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + @Composable + override fun Content() { + val windowSizeClass = calculateWindowSizeClass() + val useNavRail = windowSizeClass.widthSizeClass > WindowWidthSizeClass.Compact + + TabNavigator( + BloomTab.HomeTab + ) { + val tabNavigator = LocalTabNavigator.current + + if (useNavRail) { + Row { + BloomNavigationRailBar( + tabNavigator = it, + navRailItems = listOf( + BloomTab.HomeTab, + BloomTab.CalendarTab, + BloomTab.AddTaskTab, + BloomTab.StatisticsTab, + BloomTab.SettingsTab + ) + ) + CurrentScreen() + } + } else { + Scaffold( + content = { innerPadding -> + Box( + modifier = Modifier + .padding(innerPadding) + ) { + CurrentScreen() + } + }, + floatingActionButtonPosition = FabPosition.Center, + floatingActionButton = { + FloatingActionButton( + modifier = Modifier + .offset(y = 60.dp) + .size(42.dp), + containerColor = MaterialTheme.colorScheme.primary, + onClick = { + tabNavigator.current = BloomTab.AddTaskTab + }, + elevation = FloatingActionButtonDefaults.elevation( + defaultElevation = 0.dp + ), + shape = CircleShape + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = "", + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(24.dp) + ) + } + }, + bottomBar = { + BottomNavigation( + backgroundColor = MaterialTheme.colorScheme.background + ) { + TabNavigationItem(BloomTab.HomeTab) + TabNavigationItem(BloomTab.CalendarTab) + TabNavigationItem(BloomTab.StatisticsTab) + TabNavigationItem(BloomTab.SettingsTab) + } + } + ) + } + } + } +} + +@Composable +private fun RowScope.TabNavigationItem(tab: Tab) { + val tabNavigator = LocalTabNavigator.current + val isSelected = tabNavigator.current == tab + + BottomNavigationItem( + modifier = Modifier.offset( + x = when (tab.options.index) { + (0u).toUShort() -> 0.dp + (1u).toUShort() -> (-24).dp + (2u).toUShort() -> 24.dp + (3u).toUShort() -> 0.dp + else -> 0.dp + } + ), + selected = tabNavigator.current == tab, + onClick = { tabNavigator.current = tab }, + icon = { + tab.options.icon?.let { + Icon( + painter = if (isSelected) { + FilledIcon(tab) + } else { + it + }, + contentDescription = tab.options.title + ) + } + } + ) +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/main/MainViewModel.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/main/MainViewModel.kt new file mode 100644 index 0000000..187c22b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/main/MainViewModel.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.main + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import com.joelkanyi.focusbloom.core.domain.repository.settings.SettingsRepository +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class MainViewModel( + settingsRepository: SettingsRepository +) : ScreenModel { + + val appTheme: StateFlow = settingsRepository.getAppTheme().map { it }.stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + val onBoardingCompleted: StateFlow = + settingsRepository.getUsername().map { + OnBoardingState.Success(it.isNullOrEmpty().not()) + }.stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + initialValue = OnBoardingState.Loading + ) +} + +sealed class OnBoardingState { + data object Loading : OnBoardingState() + data class Success(val completed: Boolean) : OnBoardingState() +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/onboarding/OnBoardingScreenOne.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/onboarding/OnBoardingScreenOne.kt deleted file mode 100644 index cb8f3b9..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/onboarding/OnBoardingScreenOne.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.joelkanyi.focusbloom.onboarding - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -@Composable -fun OnBoardingScreenOne() { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = "Onboarding One Screen") - } -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/onboarding/OnBoardingScreenThree.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/onboarding/OnBoardingScreenThree.kt deleted file mode 100644 index f098b51..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/onboarding/OnBoardingScreenThree.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.joelkanyi.focusbloom.onboarding - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -@Composable -fun OnBoardingScreenThree() { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = "Onboarding Three Screen") - } -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/onboarding/OnBoardingScreenTwo.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/onboarding/OnBoardingScreenTwo.kt deleted file mode 100644 index d11f5d4..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/onboarding/OnBoardingScreenTwo.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.joelkanyi.focusbloom.onboarding - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -@Composable -fun OnBoardingScreenTwo() { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = "OnBoarding Two Screen") - } -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.kt new file mode 100644 index 0000000..65ac5d9 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import com.squareup.sqldelight.db.SqlDriver + +expect class DatabaseDriverFactory { + fun createDriver(): SqlDriver +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/Font.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/Font.kt index ee79ee9..08037ec 100644 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/Font.kt +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/Font.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.platform import androidx.compose.runtime.Composable diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.kt new file mode 100644 index 0000000..bc9b254 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.ObservableSettings + +expect class MultiplatformSettingsWrapper { + @OptIn(ExperimentalSettingsApi::class) + fun createSettings(): ObservableSettings +} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.kt new file mode 100644 index 0000000..dc2da0b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +expect fun StatusBarColors(statusBarColor: Color, navBarColor: Color) diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/settings/SettingsScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/settings/SettingsScreen.kt deleted file mode 100644 index 640ee9f..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/settings/SettingsScreen.kt +++ /dev/null @@ -1,593 +0,0 @@ -package com.joelkanyi.focusbloom.settings - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.HourglassEmpty -import androidx.compose.material.icons.outlined.LightMode -import androidx.compose.material.icons.outlined.Notifications -import androidx.compose.material.icons.outlined.Timer -import androidx.compose.material.icons.outlined.VolumeUp -import androidx.compose.material.icons.rounded.KeyboardArrowDown -import androidx.compose.material.icons.rounded.KeyboardArrowUp -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Slider -import androidx.compose.material3.SliderDefaults -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -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.graphics.vector.ImageVector -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import com.joelkanyi.focusbloom.core.presentation.component.BloomDropDown -import com.joelkanyi.focusbloom.core.presentation.component.BloomInputTextField -import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar -import com.joelkanyi.focusbloom.domain.model.TextFieldState - -class SettingsScreen : Screen { - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - SettingsScreenContent() - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SettingsScreenContent() { - Scaffold( - topBar = { - BloomTopAppBar( - hasBackNavigation = false, - ) { - Text(text = "Settings") - } - }, - ) { paddingValues -> - val optionsOpened = mutableStateListOf("") - - LazyColumn( - modifier = Modifier.padding(paddingValues), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - items(settingsOption) { option -> - SettingCard( - title = option.title, - icon = option.icon, - content = option.content, - expanded = optionsOpened.contains(option.title), - onExpand = { - if (optionsOpened.contains(option.title)) { - optionsOpened.remove(option.title) - } else { - optionsOpened.add(option.title) - } - }, - ) - } - } - } -} - -data class Setting( - val title: String, - val icon: ImageVector, - val content: @Composable () -> Unit, -) - -@OptIn(ExperimentalMaterial3Api::class) -val settingsOption = listOf( - Setting( - title = "Focus Sessions", - icon = Icons.Outlined.HourglassEmpty, - content = { - var focusSessionMinutes by remember { mutableStateOf("") } - var shortBreakMinutes by remember { mutableStateOf("") } - var longBreakMinutes by remember { mutableStateOf("") } - var autoStartBreaks by remember { mutableStateOf(false) } - var autoStartFocusSession by remember { mutableStateOf(false) } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - SessionTime( - modifier = Modifier.weight(1f), - title = "Session", - currentValue = focusSessionMinutes, - onValueChange = { - focusSessionMinutes = it - }, - ) - SessionTime( - modifier = Modifier.weight(1f), - title = "Short Break", - currentValue = shortBreakMinutes, - onValueChange = { - shortBreakMinutes = it - }, - ) - SessionTime( - modifier = Modifier.weight(1f), - title = "Long Break", - currentValue = longBreakMinutes, - onValueChange = { - longBreakMinutes = it - }, - ) - } - Spacer(modifier = Modifier.height(12.dp)) - AutoStartSession( - title = "Auto Start Breaks", - checked = autoStartBreaks, - onCheckedChange = { - autoStartBreaks = it - }, - ) - Spacer(modifier = Modifier.height(12.dp)) - AutoStartSession( - title = "Auto Start Sessions", - checked = autoStartFocusSession, - onCheckedChange = { - autoStartFocusSession = it - }, - ) - }, - ), - Setting( - title = "Time", - icon = Icons.Outlined.Timer, - content = { - var selectedHourFormat by remember { - mutableStateOf("24-hour") - } - SoundSelection( - title = "Hour Format", - options = listOf("24-hour", "12-hour"), - selectedOption = selectedHourFormat, - onSelectOption = { - selectedHourFormat = it - }, - ) - }, - ), - Setting( - title = "Sounds", - icon = Icons.Outlined.VolumeUp, - content = { - var alarmSliderPosition by remember { mutableStateOf(0f) } - var tickingSliderPosition by remember { mutableStateOf(0f) } - var selectedAlarmSound by remember { - mutableStateOf("Nokia Tune") - } - var selectedTickingSound by remember { - mutableStateOf("White Noise") - } - SoundSelection( - title = "Alarm Sounds", - options = listOf("Nokia Tune", "Samsung Tune", "Itel Tune", "Oppo Tune"), - selectedOption = selectedAlarmSound, - onSelectOption = { - selectedAlarmSound = it - }, - ) - Slider( - value = alarmSliderPosition, - valueRange = 0f..100f, - onValueChange = { alarmSliderPosition = it }, - colors = SliderDefaults.colors( - inactiveTickColor = MaterialTheme.colorScheme.secondary, - inactiveTrackColor = MaterialTheme.colorScheme.secondary, - ), - ) - Spacer(modifier = Modifier.height(16.dp)) - SoundSelection( - title = "Ticking Sounds", - options = listOf("White Noise", "Clock Ticking"), - selectedOption = selectedTickingSound, - onSelectOption = { - selectedTickingSound = it - }, - ) - Slider( - value = tickingSliderPosition, - valueRange = 0f..100f, - onValueChange = { tickingSliderPosition = it }, - colors = SliderDefaults.colors( - inactiveTickColor = MaterialTheme.colorScheme.secondary, - inactiveTrackColor = MaterialTheme.colorScheme.secondary, - ), - ) - }, - ), - Setting( - title = "Theme", - icon = Icons.Outlined.LightMode, - content = { - var showColorDialog by remember { - mutableStateOf(false) - } - var selectedColorCard by remember { - mutableStateOf("") - } - var darkTheme by remember { - mutableStateOf(false) - } - if (showColorDialog) { - ColorsDialog( - title = "Choose $selectedColorCard Color", - onDismiss = { - showColorDialog = false - }, - onSelectColor = { - showColorDialog = false - }, - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text(text = "Sessions Color Scheme") - ColorsSelection( - onSelectSessionColor = { - showColorDialog = true - selectedColorCard = "Focus Session" - }, - onSelectShortBreakColor = { - showColorDialog = true - selectedColorCard = "Short Break" - }, - onSelectLongBreakColor = { - showColorDialog = true - selectedColorCard = "Long Break" - }, - /*currentSessionColor = SessionColor, - currentShortBreakColor = ShortBreakColor, - currentLongBreakColor = LongBreakColor,*/ - currentSessionColor = Color.Magenta, - currentShortBreakColor = Color.Cyan, - currentLongBreakColor = Color.Yellow, - ) - } - Spacer(modifier = Modifier.height(16.dp)) - AutoStartSession( - title = "App Theme (Light)", - checked = darkTheme, - onCheckedChange = { - darkTheme = it - }, - ) - }, - ), - Setting( - title = "Notifications", - icon = Icons.Outlined.Notifications, - content = { - var selectedReminderType by remember { - mutableStateOf("Both") - } - var howManyMinutesToReminder by remember { - mutableStateOf("5") - } - Row { - Text( - modifier = Modifier.fillMaxWidth(.4f), - text = "Reminder", - ) - Spacer(modifier = Modifier.height(12.dp)) - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.End, - ) { - BloomDropDown( - options = listOf("Focus Session", "Break", "Both", "None"), - selectedOption = TextFieldState(selectedReminderType), - onOptionSelected = { - selectedReminderType = it - }, - ) - Spacer(modifier = Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End, - ) { - BloomInputTextField( - modifier = Modifier.weight(1f), - value = TextFieldState(text = howManyMinutesToReminder), - onValueChange = { - howManyMinutesToReminder = it - }, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = "min") - } - } - } - }, - ), -) - -@Composable -private fun SoundSelection( - options: List, - title: String, - selectedOption: String, - onSelectOption: (String) -> Unit, -) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.fillMaxWidth(.5f), - text = title, - ) - BloomDropDown( - modifier = Modifier - .fillMaxWidth(), - options = options, - selectedOption = TextFieldState(text = selectedOption), - onOptionSelected = { - onSelectOption.toString() - }, - ) - } -} - -@Composable -fun AutoStartSession( - title: String, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit, -) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text(text = title) - Switch( - checked = checked, - onCheckedChange = onCheckedChange, - ) - } -} - -@Composable -fun SessionTime( - modifier: Modifier = Modifier, - title: String, - currentValue: String, - onValueChange: (String) -> Unit, -) { - BloomInputTextField( - modifier = modifier, - textStyle = MaterialTheme.typography.bodyMedium.copy( - textAlign = TextAlign.Start, - ), - label = { - Text( - text = title, - style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.SemiBold, - ), - ) - }, - value = TextFieldState(currentValue), - onValueChange = onValueChange, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - ), - ) -} - -@Composable -fun SettingCard( - title: String, - icon: ImageVector, - modifier: Modifier = Modifier, - content: @Composable () -> Unit, - onExpand: () -> Unit, - expanded: Boolean, -) { - Card(modifier = modifier) { - Column( - modifier = modifier.padding(16.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - imageVector = icon, - contentDescription = title, - ) - Text( - text = title, - style = MaterialTheme.typography.titleLarge, - ) - } - - IconButton(onClick = { onExpand() }) { - Icon( - imageVector = if (expanded) { - Icons.Rounded.KeyboardArrowUp - } else { - Icons.Rounded.KeyboardArrowDown - }, - contentDescription = null, - ) - } - } - AnimatedVisibility(expanded) { - Column { - Spacer(modifier = Modifier.height(8.dp)) - content() - } - } - } - } -} - -@Composable -fun ColorsSelection( - onSelectSessionColor: (Color) -> Unit, - onSelectShortBreakColor: (Color) -> Unit, - onSelectLongBreakColor: (Color) -> Unit, - currentSessionColor: Color, - currentShortBreakColor: Color, - currentLongBreakColor: Color, -) { - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - ColorCard( - color = currentSessionColor, - onClick = onSelectSessionColor, - ) - ColorCard( - color = currentShortBreakColor, - onClick = onSelectShortBreakColor, - ) - ColorCard( - color = currentLongBreakColor, - onClick = onSelectLongBreakColor, - ) - } -} - -@Composable -fun ColorCard( - modifier: Modifier = Modifier, - color: Color, - onClick: (Color) -> Unit, -) { - Box( - modifier = modifier.size(32.dp).clip(MaterialTheme.shapes.medium).background(color) - .clickable { - onClick(color) - }, - ) -} - -@Composable -fun ColorsDialog( - modifier: Modifier = Modifier, - onDismiss: () -> Unit, - onSelectColor: (Color) -> Unit, - title: String, -) { - AlertDialog( - modifier = modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.large, - icon = {}, - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = onDismiss, - title = { - Text( - modifier = Modifier.fillMaxWidth(), - text = title, - style = MaterialTheme.typography.titleMedium.copy( - textAlign = TextAlign.Center, - ), - ) - }, - text = { - LazyVerticalGrid( - columns = GridCells.Fixed(4), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - items(sessionColors) { - ColorCard( - modifier = Modifier - .padding(horizontal = 4.dp) - .size(48.dp), - color = it, - onClick = onSelectColor, - ) - } - } - }, - dismissButton = {}, - confirmButton = {}, - ) -} - -private val sessionColors = listOf( - Color.Magenta, - Color.Cyan, - Color.Yellow, - Color.Red, - Color.Green, - Color.Blue, - Color.Gray, - Color.LightGray, - Color.DarkGray, - Color.Black, - Color.White, -) - -/** - * Settings - * Focus Sessions - * - time for short, long and focus session - * - auto start breaks, sessions - * Sounds - * - alarm sound - repeat times - * - ticking sound - * Theme - * - for breaks and focus session - * Hour format - * Notification - * - reminder - last, middle task - * - how many minutes to - * - alarm name - * - add new one - */ diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/statistics/StatisticsScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/statistics/StatisticsScreen.kt deleted file mode 100644 index 4fa35cf..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/statistics/StatisticsScreen.kt +++ /dev/null @@ -1,214 +0,0 @@ -package com.joelkanyi.focusbloom.statistics - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import cafe.adriel.voyager.core.screen.Screen -import com.joelkanyi.focusbloom.core.presentation.component.BloomDropDown -import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar -import com.joelkanyi.focusbloom.core.presentation.component.durationInMinutes -import com.joelkanyi.focusbloom.core.samples.sampleTasks -import com.joelkanyi.focusbloom.domain.model.TextFieldState -import com.joelkanyi.focusbloom.domain.model.Task -import com.joelkanyi.focusbloom.statistics.component.StatsChart -import com.joelkanyi.focusbloom.statistics.component.statsData - -class StatisticsScreen : Screen { - @OptIn(ExperimentalMaterial3Api::class) - @Composable - override fun Content() { - var selectedOption by remember { mutableStateOf("This Week") } - Scaffold( - topBar = { - BloomTopAppBar( - hasBackNavigation = false, - ) { - Text(text = "Statistics") - } - }, - ) { paddingValues -> - LazyColumn( - modifier = Modifier.padding(paddingValues), - contentPadding = PaddingValues(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - item { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.fillMaxWidth(.7f), - text = "Your Statistics Graph", - style = MaterialTheme.typography.displaySmall.copy( - fontSize = 18.sp, - ), - ) - BloomDropDown( - modifier = Modifier - .fillMaxWidth() - .height(32.dp), - options = listOf( - "This Week", - "This Month", - "This Year", - ), - selectedOption = TextFieldState(selectedOption), - onOptionSelected = { - selectedOption = it - }, - shape = RoundedCornerShape(50), - ) - } - } - - item { - StatsChart( - modifier = Modifier - .fillMaxWidth(), - data = statsData, - ) - } - - item { - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = "Your History", - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold, - ), - ) - TextButton(onClick = { }) { - Text( - text = "See All", - style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary, - ), - ) - } - } - } - - items(sampleTasks) { history -> - HistoryCard( - task = history, - modifier = Modifier - .fillMaxWidth(), - ) - } - } - } - } -} - -@Composable -fun HistoryCard( - task: Task, - modifier: Modifier = Modifier, -) { - Card( - modifier = modifier, - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "task.date.format(DayFormatter),", - style = MaterialTheme.typography.displaySmall.copy( - fontSize = 14.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.secondary, - ), - ) - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = "More Options", - modifier = Modifier.size(18.dp), - ) - } - Text( - text = task.name, - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - ), - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - if (task.description != null) { - Text( - text = "task.description,", - style = MaterialTheme.typography.bodyMedium, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = "${task.durationInMinutes()} minutes", - style = MaterialTheme.typography.displaySmall.copy( - fontSize = 12.sp, - fontWeight = FontWeight.SemiBold, - ), - ) - Text( - "", - /*text = "${task.start.format(TaskTimeFormatter)} - ${ - task.end.format( - TaskTimeFormatter, - ) - }",*/ - style = MaterialTheme.typography.displaySmall.copy( - fontSize = 12.sp, - fontWeight = FontWeight.SemiBold, - ), - ) - } - } - } -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/statistics/component/StatisticsChart.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/statistics/component/StatisticsChart.kt deleted file mode 100644 index 807f50d..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/statistics/component/StatisticsChart.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.joelkanyi.focusbloom.statistics.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Color.Companion.Blue -import androidx.compose.ui.graphics.Color.Companion.Gray -import androidx.compose.ui.graphics.Color.Companion.Green -import androidx.compose.ui.graphics.Color.Companion.Magenta -import androidx.compose.ui.graphics.Color.Companion.Red -import androidx.compose.ui.graphics.Color.Companion.Yellow -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp - -@Composable -fun ChartBar( - modifier: Modifier = Modifier, - today: Boolean = false, - value: Int, - maxValue: Int, - title: String, -) { - /** - * 1. Calculate the percentage of the value compared to the max value - * 2. Multiply the percentage by the max width of the bar - * 3. Set the height of the bar to the result - * 4. Set the background color of the bar to the color - * 5. Set the modifier of the bar to the modifier - * 6. Clip the bar to a rounded corner shape - * 7. Set the width of the bar to 8.dp - */ - val height = (value.toFloat() / maxValue.toFloat()) * 100 - Column { - Box( - modifier = modifier - .width(24.dp) - .height(height.dp) - .clip(shape = RoundedCornerShape(topEnd = 16.dp, topStart = 16.dp)) - .background( - color = if (today) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f) - }, - ), - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = title, - modifier = Modifier, - style = MaterialTheme.typography.bodySmall, - textAlign = TextAlign.Center, - ) - } -} - -@Composable -fun StatsChart( - modifier: Modifier = Modifier, - data: List, -) { - val maxValue = 50 - - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceAround, - verticalAlignment = androidx.compose.ui.Alignment.Bottom, - ) { - data.forEach { chartData -> - ChartBar( - value = chartData.value, - maxValue = maxValue, - title = chartData.title, - today = chartData.today, - ) - } - } -} - -data class StatsChartData( - val value: Int, - val title: String, - val color: Color, - val today: Boolean = false, -) - -val statsData = listOf( - StatsChartData( - value = 100, - color = Red, - title = "Mon", - ), - StatsChartData( - value = 20, - color = Green, - title = "Tue", - ), - StatsChartData( - value = 100, - color = Yellow, - title = "Wed", - ), - StatsChartData( - value = 40, - color = Blue, - title = "Thu", - ), - StatsChartData( - value = 50, - color = Magenta, - title = "Fri", - today = true, - ), - StatsChartData( - value = 60, - color = Gray, - title = "Sat", - ), - StatsChartData( - value = 70, - color = Red, - title = "Sun", - ), -) diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/task/AddTaskScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/task/AddTaskScreen.kt deleted file mode 100644 index 2aafc57..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/task/AddTaskScreen.kt +++ /dev/null @@ -1,225 +0,0 @@ -package com.joelkanyi.focusbloom.task - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.DateRange -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import com.joelkanyi.focusbloom.core.presentation.component.BloomButton -import com.joelkanyi.focusbloom.core.presentation.component.BloomDropDown -import com.joelkanyi.focusbloom.core.presentation.component.BloomIncrementer -import com.joelkanyi.focusbloom.core.presentation.component.BloomInputTextField -import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar -import com.joelkanyi.focusbloom.domain.model.TextFieldState - -class AddTaskScreen : Screen { - @Composable - override fun Content() { - var taskName by remember { mutableStateOf("") } - var taskDescription by remember { mutableStateOf("") } - var date by remember { mutableStateOf("") } - var focusSessions by remember { mutableIntStateOf(0) } - val taskTypes = listOf("Work", "Study", "Personal", "Other") - var selectedOption by remember { mutableStateOf(taskTypes.last()) } - - AddTaskScreenContent( - taskOptions = taskTypes, - selectedOption = selectedOption, - taskName = taskName, - taskDescription = taskDescription, - date = date, - focusSessions = focusSessions, - onClickNavigateBack = { - // navigator.popBackStack() - }, - onDateChange = { - date = it - }, - onTaskNameChange = { - taskName = it - }, - onIncrementFocusSessions = { - focusSessions = it - }, - onClickAddTask = { - }, - onSelectedOptionChange = { - selectedOption = it - }, - onTaskDescriptionChange = { - taskDescription = it - }, - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun AddTaskScreenContent( - taskOptions: List, - selectedOption: String, - onSelectedOptionChange: (String) -> Unit, - taskName: String, - taskDescription: String, - onTaskDescriptionChange: (String) -> Unit, - date: String, - focusSessions: Int, - onClickNavigateBack: () -> Unit, - onTaskNameChange: (String) -> Unit, - onDateChange: (String) -> Unit, - onIncrementFocusSessions: (Int) -> Unit, - onClickAddTask: () -> Unit, -) { - Scaffold( - topBar = { - BloomTopAppBar( - /*hasBackNavigation = true, - navigationIcon = { - IconButton(onClick = onClickNavigateBack) { - Icon( - imageVector = Icons.Outlined.ArrowBack, - contentDescription = "Add Task Back Button", - ) - } - },*/ - ) { - Text(text = "Add Task") - } - }, - ) { paddingValues -> - LazyColumn( - modifier = Modifier.padding(paddingValues), - contentPadding = PaddingValues(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - item { - BloomInputTextField( - modifier = Modifier.fillMaxWidth(), - label = { - Text(text = "Task Name") - }, - value = TextFieldState(text = taskName), - onValueChange = onTaskNameChange, - placeholder = { - Text(text = "Enter Task Name") - }, - keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Words, - ), - ) - } - item { - BloomInputTextField( - modifier = Modifier.fillMaxWidth(), - label = { - Text(text = "Description") - }, - value = TextFieldState(text = taskDescription), - onValueChange = onTaskDescriptionChange, - placeholder = { - Text(text = "Enter Description") - }, - keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - ), - ) - } - item { - BloomInputTextField( - modifier = Modifier.fillMaxWidth(), - label = { - Text(text = "Date") - }, - value = TextFieldState(text = date), - onValueChange = onDateChange, - placeholder = { - Text(text = "Enter Date") - }, - trailingIcon = { - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Outlined.DateRange, - contentDescription = "Date Picker", - ) - } - }, - ) - } - - item { - BloomDropDown( - label = { - Text(text = "Task Type") - }, - modifier = Modifier.fillMaxWidth(), - options = taskOptions, - selectedOption = TextFieldState(selectedOption), - onOptionSelected = onSelectedOptionChange, - ) - } - - item { - Text( - modifier = Modifier.fillMaxWidth(), - text = "Focus Sessions", - style = MaterialTheme.typography.titleMedium.copy( - textAlign = TextAlign.Center, - ), - ) - } - - item { - BloomIncrementer( - modifier = Modifier.fillMaxWidth(), - onClickRemove = { - if (focusSessions > 0) { - onIncrementFocusSessions(focusSessions - 1) - } - }, - onClickAdd = { - onIncrementFocusSessions(focusSessions + 1) - }, - currentValue = focusSessions, - ) - } - - item { - Spacer(modifier = Modifier.height(16.dp)) - } - - item { - BloomButton( - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - onClick = onClickAddTask, - content = { - Text(text = "Add Task") - }, - ) - } - } - } -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/FocusTimeScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/FocusTimeScreen.kt deleted file mode 100644 index ec720a6..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/FocusTimeScreen.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.joelkanyi.focusbloom.taskprogress - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ArrowBack -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import com.joelkanyi.focusbloom.core.presentation.component.BloomTimerControls -import com.joelkanyi.focusbloom.core.presentation.component.BloomTopAppBar -import com.joelkanyi.focusbloom.core.presentation.component.TaskProgress - -data class FocusTimeScreen( - val taskId: Int, -) : Screen { - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - FocusTimeScreenContent( - onClickNavigateBack = { - navigator.pop() - }, - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun FocusTimeScreenContent(onClickNavigateBack: () -> Unit) { - Scaffold( - topBar = { - BloomTopAppBar( - hasBackNavigation = true, - navigationIcon = { - IconButton(onClick = onClickNavigateBack) { - Icon( - imageVector = Icons.Outlined.ArrowBack, - contentDescription = "Add Task Back Button", - ) - } - }, - ) - }, - ) { paddingValues -> - LazyColumn( - modifier = Modifier.padding(paddingValues), - contentPadding = PaddingValues(horizontal = 16.dp), - ) { - item { - Card( - modifier = Modifier.fillMaxWidth(), - ) { - Column( - modifier = Modifier.fillMaxWidth().padding(8.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - modifier = Modifier.fillMaxWidth(.85f), - text = "Add iOS Cocoapods Support to the Multiplatform Project Shared Gradle", - style = MaterialTheme.typography.titleSmall, - ) - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.End, - ) { - Text( - text = buildAnnotatedString { - withStyle( - style = SpanStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, - ), - ) { - append("4") - } - append("/5") - }, - ) - Text(text = "25 min") - } - } - - Text( - text = "120 minutes", - style = MaterialTheme.typography.bodySmall, - ) - } - } - } - - item { - Spacer(modifier = Modifier.height(32.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { - TaskProgress( - percentage = 36f, - radius = 50.dp, - content = "23:54", - mainColor = MaterialTheme.colorScheme.primary, - ) - } - } - - item { - Spacer(modifier = Modifier.height(48.dp)) - Text( - modifier = Modifier.fillMaxWidth(), - text = "Focus Time", - style = MaterialTheme.typography.displaySmall, - textAlign = TextAlign.Center, - ) - } - - item { - Spacer(modifier = Modifier.height(56.dp)) - BloomTimerControls( - modifier = Modifier.fillMaxWidth(), - onClickReset = { /*TODO*/ }, - onClickNext = { /*TODO*/ }, - onClickAction = { /*TODO*/ }, - ) - } - } - } -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/LongBreakScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/LongBreakScreen.kt deleted file mode 100644 index 9982988..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/LongBreakScreen.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.joelkanyi.focusbloom.taskprogress - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -@Composable -fun LongBreakScreen( - taskId: Int, -) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = "Long Break Screen") - } -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/ShortBreakScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/ShortBreakScreen.kt deleted file mode 100644 index 5746053..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/ShortBreakScreen.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.joelkanyi.focusbloom.taskprogress - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -@Composable -fun ShortBreakScreen( - taskId: Int, -) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = "Short Break Screen") - } -} diff --git a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/SuccessScreen.kt b/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/SuccessScreen.kt deleted file mode 100644 index bfddd56..0000000 --- a/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/taskprogress/SuccessScreen.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.joelkanyi.focusbloom.taskprogress - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -@Composable -fun SuccessScreen() { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = "Success Screen") - } -} diff --git a/shared/src/commonMain/resources/end_time.xml b/shared/src/commonMain/resources/end_time.xml new file mode 100644 index 0000000..e3a459d --- /dev/null +++ b/shared/src/commonMain/resources/end_time.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/shared/src/commonMain/resources/ic_complete.xml b/shared/src/commonMain/resources/ic_complete.xml new file mode 100644 index 0000000..1d3128b --- /dev/null +++ b/shared/src/commonMain/resources/ic_complete.xml @@ -0,0 +1,5 @@ + + + + diff --git a/shared/src/commonMain/resources/ic_school.xml b/shared/src/commonMain/resources/ic_school.xml new file mode 100644 index 0000000..8106753 --- /dev/null +++ b/shared/src/commonMain/resources/ic_school.xml @@ -0,0 +1,10 @@ + + + diff --git a/shared/src/commonMain/resources/ic_time.xml b/shared/src/commonMain/resources/ic_time.xml new file mode 100644 index 0000000..8ba120f --- /dev/null +++ b/shared/src/commonMain/resources/ic_time.xml @@ -0,0 +1,10 @@ + + + diff --git a/shared/src/commonMain/resources/ic_work.xml b/shared/src/commonMain/resources/ic_work.xml new file mode 100644 index 0000000..25c5e42 --- /dev/null +++ b/shared/src/commonMain/resources/ic_work.xml @@ -0,0 +1,10 @@ + + + diff --git a/shared/src/commonMain/resources/il_completed.xml b/shared/src/commonMain/resources/il_completed.xml new file mode 100644 index 0000000..03f1d5f --- /dev/null +++ b/shared/src/commonMain/resources/il_completed.xml @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shared/src/commonMain/resources/il_empty.xml b/shared/src/commonMain/resources/il_empty.xml new file mode 100644 index 0000000..e6145df --- /dev/null +++ b/shared/src/commonMain/resources/il_empty.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + diff --git a/shared/src/commonMain/resources/il_statistics.xml b/shared/src/commonMain/resources/il_statistics.xml new file mode 100644 index 0000000..7032a4a --- /dev/null +++ b/shared/src/commonMain/resources/il_statistics.xml @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shared/src/commonMain/resources/il_tasks.xml b/shared/src/commonMain/resources/il_tasks.xml new file mode 100644 index 0000000..1a32d9e --- /dev/null +++ b/shared/src/commonMain/resources/il_tasks.xml @@ -0,0 +1,460 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shared/src/commonMain/resources/il_work_time.xml b/shared/src/commonMain/resources/il_work_time.xml new file mode 100644 index 0000000..e406875 --- /dev/null +++ b/shared/src/commonMain/resources/il_work_time.xml @@ -0,0 +1,619 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shared/src/commonMain/resources/other.xml b/shared/src/commonMain/resources/other.xml new file mode 100644 index 0000000..bca23a9 --- /dev/null +++ b/shared/src/commonMain/resources/other.xml @@ -0,0 +1,4 @@ + + + diff --git a/shared/src/commonMain/resources/personal.xml b/shared/src/commonMain/resources/personal.xml new file mode 100644 index 0000000..12085d0 --- /dev/null +++ b/shared/src/commonMain/resources/personal.xml @@ -0,0 +1,4 @@ + + + diff --git a/shared/src/commonMain/resources/redo.xml b/shared/src/commonMain/resources/redo.xml new file mode 100644 index 0000000..f808fb7 --- /dev/null +++ b/shared/src/commonMain/resources/redo.xml @@ -0,0 +1,11 @@ + + + diff --git a/shared/src/commonMain/resources/start_time.xml b/shared/src/commonMain/resources/start_time.xml new file mode 100644 index 0000000..5ade76e --- /dev/null +++ b/shared/src/commonMain/resources/start_time.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/shared/src/commonMain/resources/study.xml b/shared/src/commonMain/resources/study.xml new file mode 100644 index 0000000..78dc6d7 --- /dev/null +++ b/shared/src/commonMain/resources/study.xml @@ -0,0 +1,4 @@ + + + diff --git a/shared/src/commonMain/resources/work.xml b/shared/src/commonMain/resources/work.xml new file mode 100644 index 0000000..13ad7e4 --- /dev/null +++ b/shared/src/commonMain/resources/work.xml @@ -0,0 +1,4 @@ + + + diff --git a/shared/src/commonMain/sqldelight/database/task.sq b/shared/src/commonMain/sqldelight/database/task.sq new file mode 100644 index 0000000..04009d0 --- /dev/null +++ b/shared/src/commonMain/sqldelight/database/task.sq @@ -0,0 +1,71 @@ +import java.lang.String; + + +CREATE TABLE taskEntity ( + id INTEGER AS Int NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + type TEXT NOT NULL, + start TEXT NOT NULL, + color INTEGER AS Long NOT NULL, + current TEXT AS String NOT NULL, + date TEXT NOT NULL, + focusSessions INTEGER AS Int NOT NULL, + currentCycle INTEGER AS Int NOT NULL, + completed INTEGER AS Boolean NOT NULL, + consumedFocusTime INTEGER AS Long NOT NULL, + consumedShortBreakTime INTEGER AS Long NOT NULL, + consumedLongBreakTime INTEGER AS Long NOT NULL, + inProgressTask INTEGER AS Boolean NOT NULL, + active INTEGER AS Boolean NOT NULL +); + +getAllTasks: +SELECT * FROM taskEntity ORDER BY date DESC; + +getTaskById: +SELECT * FROM taskEntity WHERE id = ?; + +getActiveTask: +SELECT * FROM taskEntity WHERE active = 1; + +insertTask: +INSERT OR REPLACE +INTO taskEntity (name, description, type, start, color, current, date, focusSessions, currentCycle, completed, consumedFocusTime, consumedShortBreakTime, consumedLongBreakTime, inProgressTask, active) +VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); + +updateTask: +UPDATE taskEntity SET name = ?, description = ?, start = ?, color = ?, current = ?, date = ?, focusSessions = ?, completed = ?, active = ? WHERE id = ?; + +updateConsumedFocusTime: +UPDATE taskEntity SET consumedFocusTime = ? WHERE id = ?; + +updateConsumedShortBreakTime: +UPDATE taskEntity SET consumedShortBreakTime = ? WHERE id = ?; + +updateConsumedLongBreakTime: +UPDATE taskEntity SET consumedLongBreakTime = ? WHERE id = ?; + +updateInProgressTask: +UPDATE taskEntity SET inProgressTask = ? WHERE id = ?; + +updateCurrentSessionName: +UPDATE taskEntity SET current = ? WHERE id = ?; + +updateTaskCompleted: +UPDATE taskEntity SET completed = ? WHERE id = ?; + +updateTaskCycleNumber: +UPDATE taskEntity SET currentCycle = ? WHERE id = ?; + +updateTaskActiveStatus: +UPDATE taskEntity SET active = ? WHERE id = ?; + +updateAllTasksActiveStatusToInactive: +UPDATE taskEntity SET active = 0; + +deleteTaskById: +DELETE FROM taskEntity WHERE id = ?; + +deleteAllTasks: +DELETE FROM taskEntity; diff --git a/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/di/Module.kt b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/di/Module.kt new file mode 100644 index 0000000..ba7c0e7 --- /dev/null +++ b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/di/Module.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.di + +import com.joelkanyi.focusbloom.platform.DatabaseDriverFactory +import com.joelkanyi.focusbloom.platform.MultiplatformSettingsWrapper +import com.russhwolf.settings.ExperimentalSettingsApi +import org.koin.core.module.Module +import org.koin.dsl.module + +@OptIn(ExperimentalSettingsApi::class) +actual fun platformModule(): Module = module { + single { MultiplatformSettingsWrapper().createSettings() } + single { DatabaseDriverFactory() } +} diff --git a/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.ios.kt b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.ios.kt new file mode 100644 index 0000000..2e1210c --- /dev/null +++ b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.ios.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import com.joelkanyi.focusbloom.database.BloomDatabase +import com.squareup.sqldelight.db.SqlDriver +import com.squareup.sqldelight.drivers.native.NativeSqliteDriver + +actual class DatabaseDriverFactory { + actual fun createDriver(): SqlDriver { + return NativeSqliteDriver(schema = BloomDatabase.Schema, name = "bloom.db") + } +} diff --git a/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/Font.ios.kt b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/Font.ios.kt index ec9b5ce..4224e30 100644 --- a/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/Font.ios.kt +++ b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/Font.ios.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.platform import androidx.compose.runtime.Composable diff --git a/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/Main.kt b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/Main.kt index 7041a59..900bcf4 100644 --- a/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/Main.kt +++ b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/Main.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.platform import androidx.compose.ui.window.ComposeUIViewController diff --git a/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.ios.kt b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.ios.kt new file mode 100644 index 0000000..e952680 --- /dev/null +++ b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.ios.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import com.russhwolf.settings.AppleSettings +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.ObservableSettings +import platform.Foundation.NSUserDefaults + +actual class MultiplatformSettingsWrapper { + @OptIn(ExperimentalSettingsApi::class) + actual fun createSettings(): ObservableSettings { + val nsUserDefault = NSUserDefaults.standardUserDefaults + return AppleSettings(delegate = nsUserDefault) + } +} diff --git a/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.ios.kt b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.ios.kt new file mode 100644 index 0000000..98f0f3c --- /dev/null +++ b/shared/src/iosMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.ios.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.zeroValue +import platform.CoreGraphics.CGRect +import platform.UIKit.UIApplication +import platform.UIKit.UIColor +import platform.UIKit.UINavigationBar +import platform.UIKit.UIView +import platform.UIKit.UIWindow +import platform.UIKit.statusBarManager + +@OptIn(ExperimentalForeignApi::class) +@Composable +private fun statusBarView() = remember { + val keyWindow: UIWindow? = + UIApplication.sharedApplication.windows.firstOrNull { (it as? UIWindow)?.isKeyWindow() == true } as? UIWindow + val tag = + 3848245L // https://stackoverflow.com/questions/56651245/how-to-change-the-status-bar-background-color-and-text-color-on-ios-13 + + keyWindow?.viewWithTag(tag)?.let { + it + } ?: run { + val height = + keyWindow?.windowScene?.statusBarManager?.statusBarFrame ?: zeroValue() + val statusBarView = UIView(frame = height) + statusBarView.tag = tag + statusBarView.layer.zPosition = 999999.0 + keyWindow?.addSubview(statusBarView) + statusBarView + } +} + +@Composable +actual fun StatusBarColors(statusBarColor: Color, navBarColor: Color) { + val statusBar = statusBarView() + SideEffect { + statusBar.backgroundColor = statusBarColor.toUIColor() + UINavigationBar.appearance().backgroundColor = navBarColor.toUIColor() + } +} + +private fun Color.toUIColor(): UIColor = UIColor( + red = this.red.toDouble(), + green = this.green.toDouble(), + blue = this.blue.toDouble(), + alpha = this.alpha.toDouble() +) diff --git a/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/di/Module.kt b/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/di/Module.kt new file mode 100644 index 0000000..ba7c0e7 --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/di/Module.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.di + +import com.joelkanyi.focusbloom.platform.DatabaseDriverFactory +import com.joelkanyi.focusbloom.platform.MultiplatformSettingsWrapper +import com.russhwolf.settings.ExperimentalSettingsApi +import org.koin.core.module.Module +import org.koin.dsl.module + +@OptIn(ExperimentalSettingsApi::class) +actual fun platformModule(): Module = module { + single { MultiplatformSettingsWrapper().createSettings() } + single { DatabaseDriverFactory() } +} diff --git a/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.jvm.kt b/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.jvm.kt new file mode 100644 index 0000000..e62082f --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/DatabaseDriverFactory.jvm.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import com.joelkanyi.focusbloom.database.BloomDatabase +import com.squareup.sqldelight.db.SqlDriver +import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver + +actual class DatabaseDriverFactory { + actual fun createDriver(): SqlDriver { + return JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + .also { BloomDatabase.Schema.create(it) } + } +} diff --git a/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/Font.jvm.kt b/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/Font.jvm.kt index f8c70d5..0413710 100644 --- a/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/Font.jvm.kt +++ b/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/Font.jvm.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.joelkanyi.focusbloom.platform import androidx.compose.runtime.Composable diff --git a/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.jvm.kt b/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.jvm.kt new file mode 100644 index 0000000..f1b65fb --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.jvm.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.ExperimentalSettingsImplementation +import com.russhwolf.settings.JvmPreferencesSettings +import com.russhwolf.settings.ObservableSettings +import java.util.prefs.Preferences + +actual class MultiplatformSettingsWrapper { + @OptIn(ExperimentalSettingsImplementation::class, ExperimentalSettingsApi::class) + actual fun createSettings(): ObservableSettings { + val preferences: Preferences = Preferences.userRoot() + return JvmPreferencesSettings(delegate = preferences) + } +} diff --git a/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.jvm.kt b/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.jvm.kt new file mode 100644 index 0000000..6d1ccfe --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/joelkanyi/focusbloom/platform/StatusBarColors.jvm.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.joelkanyi.focusbloom.platform + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color + +@Composable +actual fun StatusBarColors(statusBarColor: Color, navBarColor: Color) { + SideEffect { + // no-op + } +} diff --git a/spotless/copyright.kt b/spotless/copyright.kt new file mode 100644 index 0000000..96ca41a --- /dev/null +++ b/spotless/copyright.kt @@ -0,0 +1,15 @@ +/* + * Copyright $YEAR Joel Kanyi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ \ No newline at end of file