├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── PRIVACY.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── schemas │ └── com.looker.kenko.data.local.KenkoDatabase │ │ ├── 1.json │ │ └── 2.json └── src │ ├── androidTest │ └── kotlin │ │ └── com │ │ └── looker │ │ └── kenko │ │ ├── KenkoTestRunner.kt │ │ ├── PerformanceDaoTest.kt │ │ ├── RepositoryTest.kt │ │ └── RoomDatabaseTesting.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── kenko.db │ ├── ic_launcher-playstore.png │ ├── kotlin │ │ └── com │ │ │ └── looker │ │ │ └── kenko │ │ │ ├── KenkoApp.kt │ │ │ ├── data │ │ │ ├── KenkoUriHandler.kt │ │ │ ├── StringHandler.kt │ │ │ ├── local │ │ │ │ ├── KenkoDatabase.kt │ │ │ │ ├── Migrations.kt │ │ │ │ ├── dao │ │ │ │ │ ├── ExerciseDao.kt │ │ │ │ │ ├── PerformanceDao.kt │ │ │ │ │ ├── PlanDao.kt │ │ │ │ │ ├── PlanHistoryDao.kt │ │ │ │ │ ├── SessionDao.kt │ │ │ │ │ └── SetsDao.kt │ │ │ │ ├── datastore │ │ │ │ │ └── DatastoreSettingsRepo.kt │ │ │ │ └── model │ │ │ │ │ ├── ExerciseEntity.kt │ │ │ │ │ ├── PlanEntity.kt │ │ │ │ │ ├── PlanHistoryEntity.kt │ │ │ │ │ ├── SessionEntity.kt │ │ │ │ │ ├── SetEntity.kt │ │ │ │ │ └── SetTypeEntity.kt │ │ │ ├── model │ │ │ │ ├── Exercise.kt │ │ │ │ ├── Labels.kt │ │ │ │ ├── MuscleGroups.kt │ │ │ │ ├── Plan.kt │ │ │ │ ├── PlanStat.kt │ │ │ │ ├── Rating.kt │ │ │ │ ├── Session.kt │ │ │ │ ├── Set.kt │ │ │ │ └── settings │ │ │ │ │ ├── Settings.kt │ │ │ │ │ └── Theme.kt │ │ │ └── repository │ │ │ │ ├── ExerciseRepo.kt │ │ │ │ ├── PerformanceRepo.kt │ │ │ │ ├── PlanRepo.kt │ │ │ │ ├── SessionRepo.kt │ │ │ │ ├── SettingsRepo.kt │ │ │ │ └── local │ │ │ │ ├── LocalExerciseRepo.kt │ │ │ │ ├── LocalPerformanceRepo.kt │ │ │ │ ├── LocalPlanRepo.kt │ │ │ │ └── LocalSessionRepo.kt │ │ │ ├── di │ │ │ ├── AppModule.kt │ │ │ ├── DatabaseModule.kt │ │ │ ├── DatastoreModule.kt │ │ │ ├── HandlersModule.kt │ │ │ └── RepositoryModule.kt │ │ │ ├── ui │ │ │ ├── KenkoAppState.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainViewModel.kt │ │ │ ├── addEditExercise │ │ │ │ ├── AddEditExercise.kt │ │ │ │ ├── AddEditExerciseViewModel.kt │ │ │ │ └── navigation │ │ │ │ │ └── AddEditExerciseNavigation.kt │ │ │ ├── addSet │ │ │ │ ├── AddSet.kt │ │ │ │ ├── AddSetViewModel.kt │ │ │ │ └── components │ │ │ │ │ ├── DragState.kt │ │ │ │ │ └── DraggableTextField.kt │ │ │ ├── components │ │ │ │ ├── Border.kt │ │ │ │ ├── Button.kt │ │ │ │ ├── Days.kt │ │ │ │ ├── Dp.kt │ │ │ │ ├── Labels.kt │ │ │ │ ├── List.kt │ │ │ │ ├── ReferenceItem.kt │ │ │ │ ├── Snackbar.kt │ │ │ │ ├── SwipeToDeleteBox.kt │ │ │ │ ├── Targets.kt │ │ │ │ ├── Text.kt │ │ │ │ ├── TextField.kt │ │ │ │ ├── Wave.kt │ │ │ │ └── icons │ │ │ │ │ ├── AddLarge.kt │ │ │ │ │ ├── Arrow1.kt │ │ │ │ │ ├── Arrow2.kt │ │ │ │ │ ├── Arrow3.kt │ │ │ │ │ ├── Arrow4.kt │ │ │ │ │ ├── ArrowOutwardLarge.kt │ │ │ │ │ ├── Cloud.kt │ │ │ │ │ ├── Colony.kt │ │ │ │ │ ├── ConcentricTriangles.kt │ │ │ │ │ ├── Dawn.kt │ │ │ │ │ ├── Helper.kt │ │ │ │ │ ├── QuarterCircles.kt │ │ │ │ │ ├── Reveal.kt │ │ │ │ │ ├── Stack.kt │ │ │ │ │ └── Wireframe.kt │ │ │ ├── exercises │ │ │ │ ├── Exercises.kt │ │ │ │ ├── ExercisesViewModel.kt │ │ │ │ └── navigation │ │ │ │ │ └── ExercisesNavigation.kt │ │ │ ├── extensions │ │ │ │ ├── Modifier.kt │ │ │ │ ├── PaddingValues.kt │ │ │ │ └── String.kt │ │ │ ├── getStarted │ │ │ │ ├── GetStarted.kt │ │ │ │ ├── GetStartedButton.kt │ │ │ │ ├── GetStartedOld.kt │ │ │ │ ├── GetStartedOldViewModel.kt │ │ │ │ └── navigation │ │ │ │ │ └── GetStartedNavigation.kt │ │ │ ├── home │ │ │ │ ├── Home.kt │ │ │ │ ├── HomeViewModel.kt │ │ │ │ └── navigation │ │ │ │ │ └── HomeNavigation.kt │ │ │ ├── navigation │ │ │ │ ├── KenkoNavHost.kt │ │ │ │ └── TopLevelDestinations.kt │ │ │ ├── performance │ │ │ │ ├── Performance.kt │ │ │ │ ├── PerformanceViewModel.kt │ │ │ │ ├── components │ │ │ │ │ ├── LineGraph.kt │ │ │ │ │ ├── Point.kt │ │ │ │ │ └── Points.kt │ │ │ │ └── navigation │ │ │ │ │ └── PerformanceNavigation.kt │ │ │ ├── planEdit │ │ │ │ ├── PlanEdit.kt │ │ │ │ ├── PlanEditViewModel.kt │ │ │ │ ├── PlanExercise.kt │ │ │ │ ├── PlanName.kt │ │ │ │ ├── components │ │ │ │ │ ├── DaySwitcher.kt │ │ │ │ │ └── ExerciseItem.kt │ │ │ │ └── navigation │ │ │ │ │ └── PlanEditNavigation.kt │ │ │ ├── plans │ │ │ │ ├── Plan.kt │ │ │ │ ├── PlanViewModel.kt │ │ │ │ ├── components │ │ │ │ │ └── PlanItem.kt │ │ │ │ └── navigation │ │ │ │ │ └── PlanNavigation.kt │ │ │ ├── profile │ │ │ │ ├── Profile.kt │ │ │ │ ├── ProfileViewModel.kt │ │ │ │ └── navigation │ │ │ │ │ └── ProfileNavigation.kt │ │ │ ├── selectExercise │ │ │ │ ├── SelectExercise.kt │ │ │ │ └── SelectExerciseViewModel.kt │ │ │ ├── sessionDetail │ │ │ │ ├── SessionDetail.kt │ │ │ │ ├── SessionDetailViewModel.kt │ │ │ │ ├── components │ │ │ │ │ └── SetItem.kt │ │ │ │ └── navigation │ │ │ │ │ └── SessionDetailNavigation.kt │ │ │ ├── sessions │ │ │ │ ├── Sessions.kt │ │ │ │ ├── SessionsViewModel.kt │ │ │ │ └── navigation │ │ │ │ │ └── SessionsPageNavigation.kt │ │ │ ├── settings │ │ │ │ ├── Settings.kt │ │ │ │ ├── SettingsViewModel.kt │ │ │ │ └── navigation │ │ │ │ │ └── SettingsNavigation.kt │ │ │ └── theme │ │ │ │ ├── KenkoIcons.kt │ │ │ │ ├── Shapes.kt │ │ │ │ ├── Theme.kt │ │ │ │ ├── Type.kt │ │ │ │ └── colorSchemes │ │ │ │ ├── ColorSchemes.kt │ │ │ │ ├── Default.kt │ │ │ │ ├── Serene.kt │ │ │ │ ├── Twilight.kt │ │ │ │ └── Zestful.kt │ │ │ └── utils │ │ │ ├── Collection.kt │ │ │ ├── DateTime.kt │ │ │ ├── Url.kt │ │ │ └── ViewModel.kt │ └── res │ │ ├── drawable │ │ ├── ic_add.xml │ │ ├── ic_arrow_back.xml │ │ ├── ic_arrow_forward.xml │ │ ├── ic_arrow_outward.xml │ │ ├── ic_check.xml │ │ ├── ic_close.xml │ │ ├── ic_delete.xml │ │ ├── ic_edit.xml │ │ ├── ic_history.xml │ │ ├── ic_home.xml │ │ ├── ic_info.xml │ │ ├── ic_keyboard_arrow_left.xml │ │ ├── ic_keyboard_arrow_right.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_monochrome.xml │ │ ├── ic_lightbulb.xml │ │ ├── ic_radio_button_unchecked.xml │ │ ├── ic_remove.xml │ │ ├── ic_save.xml │ │ ├── ic_settings.xml │ │ ├── ic_show_chart.xml │ │ ├── ic_tactic.xml │ │ └── ic_verified.xml │ │ ├── font │ │ ├── darkergrotesque_bold.ttf │ │ ├── darkergrotesque_semibold.ttf │ │ ├── spacemono_bold.ttf │ │ └── spacemono_normal.ttf │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-tr │ │ └── strings.xml │ │ ├── values │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── kotlin │ └── com │ └── looker │ └── kenko │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── metadata └── en-US │ ├── changelogs │ ├── 100000.txt │ ├── 101000.txt │ ├── 101010.txt │ ├── 102000.txt │ └── 103000.txt │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ ├── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ └── tvBanner.png │ └── short_description.txt └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.{kt,kts}] 9 | ktlint_code_style = android_studio 10 | indent_size = 4 11 | ij_kotlin_allow_trailing_comma=true 12 | ij_kotlin_allow_trailing_comma_on_call_site=true 13 | ij_kotlin_name_count_to_use_star_import = 999 14 | ij_kotlin_name_count_to_use_star_import_for_members = 999 15 | 16 | [*.{yml,yaml}] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | /.kotlin 12 | /kls_database.db 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file atleast once a day (if there are any changes). 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | ### Changed 12 | ### Fixed 13 | 14 | ## [1.3.0] - 2025-01-17 15 | 16 | ### Added 17 | - Drag text field in "Add Set" 18 | - Double tap to edit "set info" 19 | - History Icon (You can check last week's session if it exists) 20 | - Support for Monochrome icon on Android 12+ 21 | - Text animation on Onboarding 22 | - Safer way to delete Sets / Exercises / Plans 23 | - New Font for headings 24 | 25 | ### Changed 26 | - Targets Android 15 27 | - Onboarding screen 28 | - Default theme for new users 29 | - Sorting of muscle groups chips 30 | - Always save plan on going back 31 | - Color in Profile 32 | - Home Screen and On-boarding Screen 33 | - Some buttons and UI elements 34 | 35 | ### Fixed 36 | - Save button not visible 37 | - Two `Default` theme in Settings 38 | - Scrolling on `Select Exercise` Sheet 39 | - Performance issues on `Add Set` Sheet 40 | - Weird line in the setting wave 41 | - Crash on deleting plan 42 | - On boarding not completing 43 | 44 | ### Removed 45 | - Gradient in settings 46 | 47 | ## [1.2.0] - 2024-05-26 48 | 49 | ### Added 50 | - Support for isometric exercises 51 | - Deleting Sets / Exercises / Plans 52 | 53 | ### Changed 54 | - Error message height 55 | - Chips type in `Select Exercise` 56 | 57 | ### Fixed 58 | - Navigation to same page again 59 | - Double back presses 60 | - Swipe gesture on reps and weight text field 61 | - Elements squashing on small screens 62 | - Empty exercises 63 | - Invalid reference 64 | - False reference icon 65 | 66 | ## [1.1.1] - 2024-05-19 67 | 68 | ### Fixed 69 | - Navigation from home screen 70 | - Annoying animations on home page 71 | - Plan Edit Page 72 | - Back button on all pages 73 | 74 | ## [1.1.0] - 2024-05-19 75 | 76 | ### Added 77 | - New Home Page 78 | - Back button on Exercises Page 79 | - Option to open References from workout page(if added) 80 | 81 | ### Changed 82 | - Splash Screen Image to reduce dependency on `NonFreeNet` 83 | - Whole Plan card is clickable 84 | 85 | ### Fixed 86 | - APK dependency tree encryption 87 | - Color of icons on some buttons 88 | - `Zestful` Color Palettes 89 | - Crash when using invalid reference 90 | - UI/UX for Exercises Page 91 | - Some navigation crashes 92 | 93 | ## [1.0.0] - 2024-05-12 94 | 95 | ### Added 96 | - Initial Release 97 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | **Privacy Policy** 2 | 3 | LooKeR built the Kenko app as an Open Source app. This SERVICE is provided by LooKeR at no cost and is intended for use as is. 4 | 5 | This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service. 6 | 7 | This app does not collect or share any sort of data/information of its user. No Internet Permission is asked/requested by the app. 8 | 9 | The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which are accessible at Kenko unless otherwise defined in this Privacy Policy. 10 | 11 | **Information Collection and Use** 12 | 13 | The app does use third-party services that may collect information used to identify you. 14 | 15 | Link to the privacy policy of third-party service providers used by the app 16 | 17 | * [Google Play Services](https://www.google.com/policies/privacy/) 18 | 19 | **Security** 20 | 21 | I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. And hence we do not request or use Internet permission so no sort of data breach or collection can happen due to any sort of issues from my side. 22 | 23 | **Children’s Privacy** 24 | 25 | These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13 years of age. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do the necessary actions. 26 | 27 | **Changes to This Privacy Policy** 28 | 29 | I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. 30 | 31 | This policy is effective as of 2022-05-31 32 | 33 | **Contact Us** 34 | 35 | If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at mohit2002ss@gmail.com. 36 | 37 | This privacy policy page was created at [privacypolicytemplate.net](https://privacypolicytemplate.net) and modified/generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.nisrulz.com/) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Kenko 4 | 5 | Kenko is a workout journal which will provide you with appropriate progressive-overload and well 6 | thought-out plans 7 | 8 |
9 | 10 |
11 | 12 | ## Screenshots 13 | 14 | 15 | 16 | ## TODO 17 | 18 | - [ ] Add Rating System 19 | - [ ] Provide Targeted Overload 20 | - [ ] Add Import/Export 21 | - [x] Add Support for Isometric exercises 22 | 23 | ## CONTRIBUTIONS 24 | 25 | ## LICENSE 26 | 27 | ``` 28 | Kenko 29 | 30 | Copyright (C) 2024 LooKeR & Contributors 31 | This program is free software: you can redistribute it and/or modify 32 | it under the terms of the GNU General Public License as published by 33 | the Free Software Foundation, either version 3 of the License, or 34 | (at your option) any later version. 35 | This program is distributed in the hope that it will be useful, 36 | but WITHOUT ANY WARRANTY; without even the implied warranty of 37 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 38 | GNU General Public License for more details. 39 | You should have received a copy of the GNU General Public License 40 | along with this program. If not, see . 41 | ``` 42 | 43 |
44 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | /reports 4 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/kotlin/com/looker/kenko/KenkoTestRunner.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko 16 | 17 | import android.app.Application 18 | import android.content.Context 19 | import androidx.test.runner.AndroidJUnitRunner 20 | import dagger.hilt.android.testing.HiltTestApplication 21 | import kotlin.math.pow 22 | import kotlin.math.sqrt 23 | 24 | class KenkoTestRunner : AndroidJUnitRunner() { 25 | override fun newApplication( 26 | cl: ClassLoader, 27 | appName: String, 28 | context: Context, 29 | ): Application { 30 | return super.newApplication( 31 | cl, 32 | HiltTestApplication::class.java.name, 33 | context, 34 | ) 35 | } 36 | } 37 | 38 | internal inline fun benchmark( 39 | repetition: Int, 40 | extraMessage: String? = null, 41 | block: () -> Long, 42 | ): String { 43 | if (extraMessage != null) { 44 | println("=".repeat(50)) 45 | println(extraMessage) 46 | println("=".repeat(50)) 47 | } 48 | val times = DoubleArray(repetition) 49 | repeat(repetition) { iteration -> 50 | System.gc() 51 | System.runFinalization() 52 | times[iteration] = block().toDouble() 53 | } 54 | val meanAndDeviation = times.culledMeanAndDeviation() 55 | return buildString { 56 | append("=".repeat(50)) 57 | append("\n") 58 | append(times.joinToString(" | ")) 59 | append("\n") 60 | append("${meanAndDeviation.first} ms ± ${meanAndDeviation.second.toFloat()} ms") 61 | append("\n") 62 | append("=".repeat(50)) 63 | append("\n") 64 | } 65 | } 66 | 67 | private fun DoubleArray.culledMeanAndDeviation(): Pair { 68 | sort() 69 | return meanAndDeviation() 70 | } 71 | 72 | private fun DoubleArray.meanAndDeviation(): Pair { 73 | val mean = average() 74 | return mean to sqrt(fold(0.0) { acc, value -> acc + (value - mean).pow(2) } / size) 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/assets/kenko.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/assets/kenko.db -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/KenkoApp.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class KenkoApp : Application() 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/KenkoUriHandler.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import androidx.compose.ui.platform.UriHandler 8 | import com.looker.kenko.R 9 | 10 | class KenkoUriHandler(private val context: Context) : UriHandler { 11 | override fun openUri(uri: String) { 12 | try { 13 | val intent = Intent( 14 | Intent.ACTION_VIEW, 15 | Uri.parse(uri) 16 | ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 17 | context.startActivity(intent) 18 | } catch (e: ActivityNotFoundException) { 19 | error(context.getString(R.string.error_invalid_url)) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/StringHandler.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | 6 | class StringHandler(context: Context) { 7 | 8 | private val resources: Resources = context.resources 9 | 10 | fun getString(id: Int): String { 11 | resources.configuration 12 | return resources.getString(id) 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/KenkoDatabase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.local 16 | 17 | import android.content.Context 18 | import androidx.room.Database 19 | import androidx.room.Room 20 | import androidx.room.RoomDatabase 21 | import com.looker.kenko.data.local.dao.ExerciseDao 22 | import com.looker.kenko.data.local.dao.PerformanceDao 23 | import com.looker.kenko.data.local.dao.PlanDao 24 | import com.looker.kenko.data.local.dao.PlanHistoryDao 25 | import com.looker.kenko.data.local.dao.SessionDao 26 | import com.looker.kenko.data.local.dao.SetsDao 27 | import com.looker.kenko.data.local.model.ExerciseEntity 28 | import com.looker.kenko.data.local.model.PlanDayEntity 29 | import com.looker.kenko.data.local.model.PlanEntity 30 | import com.looker.kenko.data.local.model.PlanHistoryEntity 31 | import com.looker.kenko.data.local.model.SessionDataEntity 32 | import com.looker.kenko.data.local.model.SetEntity 33 | import com.looker.kenko.data.local.model.SetTypeEntity 34 | 35 | @Database( 36 | version = 2, 37 | entities = [ 38 | SessionDataEntity::class, 39 | ExerciseEntity::class, 40 | PlanEntity::class, 41 | PlanHistoryEntity::class, 42 | PlanDayEntity::class, 43 | SetEntity::class, 44 | SetTypeEntity::class, 45 | ], 46 | ) 47 | abstract class KenkoDatabase : RoomDatabase() { 48 | abstract fun sessionDao(): SessionDao 49 | abstract fun exerciseDao(): ExerciseDao 50 | abstract fun planDao(): PlanDao 51 | abstract fun setsDao(): SetsDao 52 | abstract fun historyDao(): PlanHistoryDao 53 | abstract fun performanceDao(): PerformanceDao 54 | } 55 | 56 | fun kenkoDatabase(context: Context) = Room 57 | .databaseBuilder( 58 | context = context, 59 | klass = KenkoDatabase::class.java, 60 | name = "kenko_database", 61 | ) 62 | .createFromAsset("kenko.db") 63 | .addMigrations( 64 | MIGRATION_1_2, 65 | ) 66 | .build() 67 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/dao/ExerciseDao.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data.local.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import androidx.room.Upsert 6 | import com.looker.kenko.data.local.model.ExerciseEntity 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao 10 | interface ExerciseDao { 11 | 12 | @Upsert 13 | suspend fun upsert(exercise: ExerciseEntity) 14 | 15 | @Query( 16 | """ 17 | DELETE 18 | FROM exercises 19 | WHERE id = :id 20 | """, 21 | ) 22 | suspend fun delete(id: Int) 23 | 24 | @Query( 25 | """ 26 | SELECT * 27 | FROM exercises 28 | """, 29 | ) 30 | fun stream(): Flow> 31 | 32 | @Query( 33 | """ 34 | SELECT * 35 | FROM exercises 36 | WHERE id = :id 37 | """, 38 | ) 39 | suspend fun get(id: Int): ExerciseEntity? 40 | 41 | @Query( 42 | """ 43 | SELECT COUNT(*) 44 | FROM exercises 45 | """, 46 | ) 47 | fun number(): Flow 48 | 49 | @Query( 50 | """ 51 | SELECT EXISTS 52 | (SELECT * 53 | FROM exercises 54 | WHERE name = :name) 55 | """, 56 | ) 57 | suspend fun exists(name: String): Boolean 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/dao/PerformanceDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.local.dao 16 | 17 | import androidx.room.Dao 18 | import androidx.room.RawQuery 19 | import androidx.room.Transaction 20 | import androidx.room.Upsert 21 | import androidx.sqlite.db.SimpleSQLiteQuery 22 | import com.looker.kenko.data.local.model.SetTypeEntity 23 | import com.looker.kenko.data.repository.Performance 24 | 25 | @Dao 26 | interface PerformanceDao { 27 | 28 | @Upsert 29 | suspend fun upsertSetTypeLookup(type: List) 30 | 31 | @RawQuery 32 | suspend fun _rawQueryRatingWrappers(query: SimpleSQLiteQuery): List? 33 | 34 | @Transaction 35 | suspend fun getPerformance(exerciseId: Int?, planId: Int?): Performance? { 36 | val selection = arrayListOf() 37 | val query = buildString(512) { 38 | append("SELECT sessions.date, sets.reps * sets.weight * set_type.modifier AS rating FROM sets ") 39 | append("INNER JOIN set_type ON sets.type = set_type.type ") 40 | append("INNER JOIN sessions ON sets.sessionId = sessions.id ") 41 | if (exerciseId != null) { 42 | append("WHERE (sets.exerciseId = ?) ") 43 | selection.add(exerciseId) 44 | } 45 | if (planId != null) { 46 | if (exerciseId != null) { 47 | append("AND ") 48 | } else { 49 | append("WHERE ") 50 | } 51 | append("sessionId IN (SELECT id FROM sessions WHERE planId = ?) ") 52 | selection.add(planId) 53 | } 54 | append("ORDER BY sessions.date ASC") 55 | } 56 | val ratingWrapper = _rawQueryRatingWrappers( 57 | SimpleSQLiteQuery( 58 | query = query, 59 | bindArgs = selection.toTypedArray(), 60 | ), 61 | ) 62 | return ratingWrapper?.toPerformance() 63 | } 64 | 65 | } 66 | 67 | class RatingWrapper( 68 | val date: Int, 69 | val rating: Float, 70 | ) 71 | 72 | fun List.toPerformance(): Performance { 73 | val days = IntArray(size) 74 | val ratings = FloatArray(size) 75 | for (i in indices) { 76 | val rating = get(i) 77 | days[i] = rating.date 78 | ratings[i] = rating.rating 79 | } 80 | return Performance( 81 | days = days, 82 | ratings = ratings, 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/dao/PlanHistoryDao.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data.local.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import androidx.room.Upsert 6 | import com.looker.kenko.data.local.model.PlanHistoryEntity 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao 10 | interface PlanHistoryDao { 11 | 12 | @Query( 13 | """ 14 | SELECT planId 15 | FROM plan_history 16 | WHERE `end` IS NULL 17 | AND start IS NOT NULL 18 | """, 19 | ) 20 | fun currentIdFlow(): Flow 21 | 22 | @Query( 23 | """ 24 | SELECT planId 25 | FROM plan_history 26 | WHERE `end` IS NULL 27 | AND start IS NOT NULL 28 | """, 29 | ) 30 | suspend fun getCurrentId(): Int? 31 | 32 | @Query( 33 | """ 34 | SELECT * 35 | FROM plan_history 36 | WHERE `end` IS NULL 37 | AND start IS NOT NULL 38 | """, 39 | ) 40 | fun currentFlow(): Flow 41 | 42 | @Query( 43 | """ 44 | SELECT * 45 | FROM plan_history 46 | WHERE `end` IS NULL 47 | AND start IS NOT NULL 48 | """, 49 | ) 50 | suspend fun getCurrent(): PlanHistoryEntity? 51 | 52 | @Query( 53 | """ 54 | SELECT * 55 | FROM plan_history 56 | """ 57 | ) 58 | suspend fun getAll(): List 59 | 60 | @Upsert 61 | suspend fun upsert(history: PlanHistoryEntity) 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/dao/SessionDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.local.dao 16 | 17 | import androidx.room.Dao 18 | import androidx.room.Insert 19 | import androidx.room.OnConflictStrategy 20 | import androidx.room.Query 21 | import androidx.room.Transaction 22 | import com.looker.kenko.data.local.model.SessionDataEntity 23 | import com.looker.kenko.data.local.model.SessionEntity 24 | import com.looker.kenko.utils.EpochDays 25 | import kotlinx.coroutines.flow.Flow 26 | 27 | @Dao 28 | interface SessionDao { 29 | 30 | @Insert(onConflict = OnConflictStrategy.IGNORE) 31 | suspend fun insert(session: SessionDataEntity): Long 32 | 33 | @Query( 34 | """ 35 | SELECT EXISTS 36 | (SELECT * 37 | FROM sessions 38 | WHERE date = :date) 39 | """, 40 | ) 41 | suspend fun sessionExistsOn(date: EpochDays): Boolean 42 | 43 | @Query( 44 | """ 45 | SELECT date 46 | FROM sessions 47 | WHERE id = :sessionId 48 | """, 49 | ) 50 | suspend fun getDatePerformedOn(sessionId: Int): EpochDays 51 | 52 | @Query( 53 | """ 54 | SELECT COUNT(*) 55 | FROM sessions 56 | """, 57 | ) 58 | suspend fun getTotalSessions(): Int 59 | 60 | @Query( 61 | """ 62 | SELECT id 63 | FROM sessions 64 | WHERE date = :date 65 | """, 66 | ) 67 | suspend fun getSessionId(date: EpochDays): Int? 68 | 69 | @Transaction 70 | @Query( 71 | """ 72 | SELECT * 73 | FROM sessions 74 | """, 75 | ) 76 | fun stream(): Flow> 77 | 78 | @Transaction 79 | @Query( 80 | """ 81 | SELECT * 82 | FROM sessions 83 | WHERE date = :date 84 | """, 85 | ) 86 | fun session(date: EpochDays): Flow 87 | 88 | @Transaction 89 | @Query( 90 | """ 91 | SELECT * 92 | FROM sessions 93 | WHERE date = :date 94 | """, 95 | ) 96 | suspend fun getSession(date: EpochDays): SessionEntity? 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/dao/SetsDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.local.dao 16 | 17 | import androidx.room.Dao 18 | import androidx.room.Insert 19 | import androidx.room.Query 20 | import com.looker.kenko.data.local.model.SetEntity 21 | import kotlinx.coroutines.flow.Flow 22 | 23 | @Dao 24 | interface SetsDao { 25 | 26 | @Query( 27 | """ 28 | SELECT * 29 | FROM sets 30 | WHERE sessionId = :sessionId 31 | ORDER BY `order` 32 | """, 33 | ) 34 | fun setsBySessionId(sessionId: Int): Flow> 35 | 36 | @Query( 37 | """ 38 | SELECT * 39 | FROM sets 40 | WHERE sessionId = :sessionId 41 | ORDER BY `order` 42 | """, 43 | ) 44 | suspend fun getSetsBySessionId(sessionId: Int): List 45 | 46 | @Query( 47 | """ 48 | SELECT COUNT(*) 49 | FROM sets 50 | WHERE sessionId = :sessionId 51 | """, 52 | ) 53 | suspend fun getSetsCountBySessionId(sessionId: Int): Int? 54 | 55 | @Query( 56 | """ 57 | SELECT * 58 | FROM sets 59 | WHERE (:exerciseId IS NULL OR exerciseId = :exerciseId) 60 | AND sessionId IN ( 61 | SELECT id 62 | FROM sessions 63 | WHERE (:planId IS NULL OR planId = :planId) 64 | ) 65 | ORDER BY `order` 66 | """, 67 | ) 68 | fun setsByExerciseIdPerPlan(exerciseId: Int? = null, planId: Int? = null): Flow> 69 | 70 | @Query( 71 | """ 72 | SELECT * 73 | FROM sets 74 | WHERE (:exerciseId IS NULL OR exerciseId = :exerciseId) 75 | AND sessionId IN ( 76 | SELECT id 77 | FROM sessions 78 | WHERE (:planId IS NULL OR planId = :planId) 79 | ) 80 | ORDER BY `order` 81 | """, 82 | ) 83 | suspend fun getSetsByExerciseIdPerPlan( 84 | exerciseId: Int? = null, 85 | planId: Int? = null, 86 | ): List 87 | 88 | @Query( 89 | """ 90 | SELECT COUNT (*) 91 | FROM sets 92 | """, 93 | ) 94 | fun totalSetCount(): Flow 95 | 96 | @Insert 97 | suspend fun insert(set: SetEntity) 98 | 99 | @Query( 100 | """ 101 | DELETE 102 | FROM sets 103 | WHERE id = :setId 104 | """, 105 | ) 106 | suspend fun delete(setId: Int) 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/datastore/DatastoreSettingsRepo.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data.local.datastore 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.core.IOException 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.booleanPreferencesKey 7 | import androidx.datastore.preferences.core.edit 8 | import androidx.datastore.preferences.core.stringPreferencesKey 9 | import com.looker.kenko.BuildConfig 10 | import com.looker.kenko.data.model.settings.ColorPalettes 11 | import com.looker.kenko.data.model.settings.Settings 12 | import com.looker.kenko.data.model.settings.Theme 13 | import com.looker.kenko.data.repository.SettingsRepo 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.flow.catch 16 | import kotlinx.coroutines.flow.map 17 | import javax.inject.Inject 18 | 19 | class DatastoreSettingsRepo @Inject constructor( 20 | private val dataStore: DataStore, 21 | ) : SettingsRepo { 22 | 23 | override val stream: Flow 24 | get() = dataStore.data 25 | .catch { if (it is IOException) error("Error reading datastore") } 26 | .map(::mapSettings) 27 | 28 | override fun get(block: Settings.() -> T): Flow { 29 | return stream.map { it.block() } 30 | } 31 | 32 | override suspend fun setOnboardingDone() { 33 | if (!BuildConfig.DEBUG) { 34 | ONBOARDING_DONE.update(true) 35 | } 36 | } 37 | 38 | override suspend fun setColorPalette(colorPalette: ColorPalettes) { 39 | COLOR_PALETTE.update(colorPalette.name) 40 | } 41 | 42 | override suspend fun setTheme(theme: Theme) { 43 | THEME.update(theme.name) 44 | } 45 | 46 | private suspend inline fun Preferences.Key.update(value: T) { 47 | dataStore.edit { preference -> 48 | preference[this] = value 49 | } 50 | } 51 | 52 | private fun mapSettings(preferences: Preferences): Settings { 53 | val isOnboardingDone = preferences[ONBOARDING_DONE] ?: false 54 | val theme = preferences[THEME] ?: Theme.System.name 55 | val colorPalettes = preferences[COLOR_PALETTE] ?: ColorPalettes.Zestful.name 56 | return Settings( 57 | isOnboardingDone = isOnboardingDone, 58 | theme = Theme.valueOf(theme), 59 | colorPalette = ColorPalettes.valueOf(colorPalettes), 60 | ) 61 | } 62 | 63 | private companion object Keys { 64 | val ONBOARDING_DONE: Preferences.Key = booleanPreferencesKey("onboarding_done") 65 | val THEME: Preferences.Key = stringPreferencesKey("theme") 66 | val COLOR_PALETTE: Preferences.Key = stringPreferencesKey("color_palette") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/model/ExerciseEntity.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data.local.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.looker.kenko.data.model.Exercise 6 | import com.looker.kenko.data.model.MuscleGroups 7 | import kotlinx.serialization.SerialName 8 | import kotlinx.serialization.Serializable 9 | 10 | @Serializable 11 | @SerialName("exercise") 12 | @Entity("exercises") 13 | data class ExerciseEntity( 14 | val name: String, 15 | val target: MuscleGroups, 16 | val reference: String? = null, 17 | val isIsometric: Boolean = false, 18 | @PrimaryKey(autoGenerate = true) 19 | val id: Int = 0 20 | ) 21 | fun ExerciseEntity.toExternal(): Exercise = Exercise( 22 | id = id, 23 | name = name, 24 | target = target, 25 | reference = reference, 26 | isIsometric = isIsometric 27 | ) 28 | 29 | fun Exercise.toEntity(): ExerciseEntity = ExerciseEntity( 30 | id = id ?: 0, 31 | name = name, 32 | target = target, 33 | reference = reference, 34 | isIsometric = isIsometric 35 | ) 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/model/PlanHistoryEntity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.local.model 16 | 17 | import androidx.room.ColumnInfo 18 | import androidx.room.Entity 19 | import androidx.room.ForeignKey 20 | import androidx.room.Index 21 | import androidx.room.PrimaryKey 22 | import com.looker.kenko.utils.EpochDays 23 | 24 | @Entity( 25 | tableName = "plan_history", 26 | foreignKeys = [ 27 | ForeignKey( 28 | entity = PlanEntity::class, 29 | parentColumns = ["id"], 30 | childColumns = ["planId"], 31 | onDelete = ForeignKey.Companion.SET_NULL 32 | ) 33 | ], 34 | indices = [ 35 | Index("planId", "start", "end") 36 | ] 37 | ) 38 | data class PlanHistoryEntity( 39 | val planId: Int?, 40 | val start: EpochDays, 41 | @ColumnInfo(defaultValue = "NULL") 42 | val end: EpochDays? = null, 43 | @PrimaryKey(autoGenerate = true) 44 | val id: Long = 0 45 | ) 46 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/model/SessionEntity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.local.model 16 | 17 | import androidx.room.ColumnInfo 18 | import androidx.room.Embedded 19 | import androidx.room.Entity 20 | import androidx.room.ForeignKey 21 | import androidx.room.PrimaryKey 22 | import androidx.room.Relation 23 | import com.looker.kenko.data.model.Session 24 | import com.looker.kenko.data.model.Set 25 | import com.looker.kenko.utils.EpochDays 26 | import kotlinx.datetime.LocalDate 27 | 28 | data class SessionEntity( 29 | @Embedded 30 | val data: SessionDataEntity, 31 | @Relation( 32 | parentColumn = "id", 33 | entityColumn = "sessionId", 34 | ) 35 | val sets: List, 36 | ) 37 | 38 | @Entity( 39 | "sessions", 40 | foreignKeys = [ 41 | ForeignKey( 42 | entity = PlanEntity::class, 43 | parentColumns = ["id"], 44 | childColumns = ["planId"], 45 | onDelete = ForeignKey.SET_NULL, 46 | ), 47 | ], 48 | ) 49 | data class SessionDataEntity( 50 | val date: EpochDays, 51 | @ColumnInfo(index = true) 52 | val planId: Int?, 53 | @PrimaryKey(autoGenerate = true) 54 | val id: Int = 0, 55 | ) 56 | 57 | fun Session.data(): SessionDataEntity = SessionDataEntity( 58 | date = EpochDays(date.toEpochDays()), 59 | planId = planId, 60 | id = id ?: 0, 61 | ) 62 | 63 | fun Session.sets(): List = sets.map { it.toEntity(id!!, sets.indexOf(it)) } 64 | 65 | fun SessionEntity.toExternal( 66 | setsMap: List, 67 | ): Session = Session( 68 | planId = data.planId, 69 | date = LocalDate.fromEpochDays(data.date.value), 70 | sets = setsMap, 71 | id = data.id, 72 | ) 73 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/model/SetEntity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.local.model 16 | 17 | import androidx.room.ColumnInfo 18 | import androidx.room.Entity 19 | import androidx.room.ForeignKey 20 | import androidx.room.Index 21 | import androidx.room.PrimaryKey 22 | import com.looker.kenko.data.model.Exercise 23 | import com.looker.kenko.data.model.Rating 24 | import com.looker.kenko.data.model.Set 25 | import com.looker.kenko.utils.sumOf 26 | 27 | @Entity( 28 | "sets", 29 | foreignKeys = [ 30 | ForeignKey( 31 | entity = ExerciseEntity::class, 32 | parentColumns = ["id"], 33 | childColumns = ["exerciseId"], 34 | onDelete = ForeignKey.CASCADE, 35 | ), 36 | ForeignKey( 37 | entity = SessionDataEntity::class, 38 | parentColumns = ["id"], 39 | childColumns = ["sessionId"], 40 | onDelete = ForeignKey.CASCADE, 41 | ), 42 | ], 43 | indices = [ 44 | Index("sessionId", "exerciseId"), 45 | ], 46 | ) 47 | data class SetEntity( 48 | @ColumnInfo("reps") 49 | val repsOrDuration: Int, 50 | val weight: Float, 51 | val type: SetType, 52 | val order: Int, 53 | val sessionId: Int, 54 | val exerciseId: Int, 55 | @PrimaryKey(autoGenerate = true) 56 | val id: Int = 0, 57 | ) 58 | 59 | val List.rating: Rating 60 | get() = Rating(sumOf { it.repsOrDuration * it.weight * it.type.ratingModifier }) 61 | 62 | val SetEntity.rating: Rating 63 | get() = Rating(repsOrDuration * weight * type.ratingModifier) 64 | 65 | fun SetEntity.toExternal(exercise: Exercise): Set = Set( 66 | repsOrDuration = repsOrDuration, 67 | weight = weight, 68 | type = type, 69 | exercise = exercise, 70 | id = id, 71 | ) 72 | 73 | fun Set.toEntity(sessionId: Int, order: Int): SetEntity = SetEntity( 74 | id = id ?: 0, 75 | repsOrDuration = repsOrDuration, 76 | weight = weight, 77 | type = type, 78 | order = order, 79 | sessionId = sessionId, 80 | exerciseId = requireNotNull(exercise.id), 81 | ) 82 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/local/model/SetTypeEntity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.local.model 16 | 17 | import androidx.room.Entity 18 | import androidx.room.PrimaryKey 19 | 20 | @Entity("set_type") 21 | data class SetTypeEntity( 22 | @PrimaryKey 23 | val type: SetType, 24 | val modifier: Float, 25 | ) 26 | 27 | enum class SetType(val ratingModifier: Float) { 28 | Standard(STANDARD_SET_RATING_MODIFIER), 29 | Drop(DROP_SET_RATING_MODIFIER), 30 | RestPause(REST_PAUSE_SET_RATING_MODIFIER), 31 | } 32 | 33 | private const val STANDARD_SET_RATING_MODIFIER: Float = 1.0F 34 | private const val DROP_SET_RATING_MODIFIER: Float = 1.35F 35 | private const val REST_PAUSE_SET_RATING_MODIFIER: Float = 1.2F 36 | 37 | fun defaultSetTypes()= listOf( 38 | SetTypeEntity(SetType.Standard, STANDARD_SET_RATING_MODIFIER), 39 | SetTypeEntity(SetType.Drop, DROP_SET_RATING_MODIFIER), 40 | SetTypeEntity(SetType.RestPause, REST_PAUSE_SET_RATING_MODIFIER), 41 | ) 42 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/model/Labels.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.model 16 | 17 | // TODO: Empahsis on these being a suggestion, "These labels are just suggestions, 18 | // even if you are advanced you can use a beginner plan 19 | // and change it to your liking and use it" 20 | // also clarify how you can recognize your own place in this system, 21 | // 1-3 yrs is beginners, and so on 22 | sealed interface Labels { 23 | enum class Difficulty : Labels { 24 | // More free weight exercises, and generally slower plans 25 | BEGINNER, 26 | INTERMEDIATE, 27 | ADVANCED, 28 | // Generally need personal additions 29 | ADAPTABLE, 30 | } 31 | 32 | enum class Focus : Labels { 33 | STRENGTH, 34 | HYPERTROPHY, 35 | POWER_BUILDING, 36 | } 37 | 38 | enum class Equipment : Labels { 39 | FULL_GYM, 40 | DUMBBELLS, 41 | BARBELLS, 42 | NONE, 43 | } 44 | 45 | enum class Time : Labels { 46 | QUICK, 47 | NORMAL, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/model/MuscleGroups.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data.model 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.runtime.Stable 5 | import com.looker.kenko.R 6 | import kotlinx.serialization.Serializable 7 | 8 | @Stable 9 | @Serializable 10 | enum class MuscleGroups(@StringRes val stringRes: Int) { 11 | // Arms 12 | Biceps(R.string.label_muscle_biceps), 13 | Triceps(R.string.label_muscle_triceps), 14 | Shoulders(R.string.label_muscle_shoulders), 15 | 16 | // Legs 17 | Quads(R.string.label_muscle_quads), 18 | Hamstrings(R.string.label_muscle_hamstrings), 19 | Calves(R.string.label_muscle_calves), 20 | Glutes(R.string.label_muscle_glutes), 21 | 22 | // Front 23 | Core(R.string.label_muscle_core), 24 | Chest(R.string.label_muscle_chest), 25 | 26 | // Back 27 | Traps(R.string.label_muscle_traps), 28 | Lats(R.string.label_muscle_lats), 29 | UpperBack(R.string.label_muscle_back), 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/model/Plan.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.model 16 | 17 | import androidx.compose.runtime.Immutable 18 | import androidx.compose.ui.tooling.preview.PreviewParameterProvider 19 | import com.looker.kenko.data.model.Labels.Difficulty 20 | import com.looker.kenko.data.model.Labels.Equipment 21 | import com.looker.kenko.data.model.Labels.Focus 22 | import com.looker.kenko.data.model.Labels.Time 23 | import kotlinx.datetime.Clock 24 | import kotlinx.datetime.DatePeriod 25 | import kotlinx.datetime.DayOfWeek 26 | import kotlinx.datetime.TimeZone 27 | import kotlinx.datetime.toLocalDateTime 28 | 29 | @Immutable 30 | data class Plan( 31 | val name: String, 32 | val description: String?, 33 | val difficulty: Difficulty?, 34 | val focus: Focus?, 35 | val equipment: Equipment?, 36 | val time: Time?, 37 | val isActive: Boolean, 38 | val stat: PlanStat = PlanStat(0, 0), 39 | val id: Int? = null, 40 | ) 41 | 42 | @Immutable 43 | data class PlanItem( 44 | val dayOfWeek: DayOfWeek, 45 | val exercise: Exercise, 46 | val planId: Int, 47 | val id: Long? = null, 48 | ) 49 | 50 | val localDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date 51 | 52 | val week = DatePeriod(days = 7) 53 | 54 | class PlanPreviewParameters : PreviewParameterProvider> { 55 | override val values: Sequence> = sequenceOf( 56 | listOf( 57 | Plan( 58 | name = "Push Pull Leg", 59 | description = null, 60 | difficulty = Difficulty.ADAPTABLE, 61 | focus = null, 62 | equipment = Equipment.FULL_GYM, 63 | time = Time.NORMAL, 64 | isActive = true, 65 | stat = PlanStat(21, 5), 66 | ), 67 | Plan( 68 | name = "Upper Lower", 69 | description = "Alternative upper lower split", 70 | difficulty = Difficulty.BEGINNER, 71 | focus = Focus.POWER_BUILDING, 72 | equipment = Equipment.FULL_GYM, 73 | time = Time.QUICK, 74 | isActive = false, 75 | stat = PlanStat(21, 4), 76 | ), 77 | Plan( 78 | name = "Upper Lower 2", 79 | description = "Lower Upper split at home", 80 | difficulty = Difficulty.ADAPTABLE, 81 | focus = Focus.POWER_BUILDING, 82 | equipment = Equipment.DUMBBELLS, 83 | time = Time.QUICK, 84 | isActive = false, 85 | stat = PlanStat(21, 5), 86 | ), 87 | ), 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/model/PlanStat.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.model 16 | 17 | import androidx.compose.runtime.Immutable 18 | import androidx.compose.runtime.Stable 19 | import androidx.compose.ui.util.packInts 20 | import androidx.compose.ui.util.unpackInt1 21 | import androidx.compose.ui.util.unpackInt2 22 | 23 | @Immutable 24 | @JvmInline 25 | value class PlanStat(private val packedInt: Long) { 26 | 27 | @Stable 28 | val exercises: Int get() = unpackInt1(packedInt) 29 | 30 | @Stable 31 | val workDays: Int get() = unpackInt2(packedInt) 32 | 33 | @Stable 34 | val restDays: Int get() = 7 - workDays 35 | } 36 | 37 | fun PlanStat(exercises: Int, workDays: Int): PlanStat { 38 | return PlanStat(packInts(exercises, workDays)) 39 | } 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/model/Rating.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.model 16 | 17 | import androidx.compose.runtime.Immutable 18 | 19 | @Immutable 20 | @JvmInline 21 | value class Rating(val value: Float) 22 | 23 | operator fun Rating.plus(other: Rating) = Rating(value + other.value) 24 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/model/Session.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.model 16 | 17 | import androidx.compose.runtime.Immutable 18 | import com.looker.kenko.utils.sumOf 19 | import kotlinx.datetime.LocalDate 20 | 21 | @Immutable 22 | data class Session( 23 | val date: LocalDate, 24 | val sets: List, 25 | val planId: Int?, 26 | val id: Int? = null, 27 | ) 28 | 29 | fun Session(planId: Int, sets: List) = Session(planId = planId, date = localDate, sets = sets) 30 | 31 | val Session.currentRating: Rating 32 | get() = Rating(sets.sumOf { it.rating.value }) 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/model/Set.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.model 16 | 17 | import androidx.compose.runtime.Immutable 18 | import com.looker.kenko.data.local.model.SetType 19 | import kotlinx.serialization.Serializable 20 | 21 | @Serializable 22 | @Immutable 23 | data class Set( 24 | val repsOrDuration: Int, 25 | val weight: Float, 26 | val type: SetType, 27 | val exercise: Exercise, 28 | val id: Int? = null, 29 | ) 30 | 31 | val Set.rating: Rating 32 | get() = Rating(repsOrDuration * weight * type.ratingModifier) 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/model/settings/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data.model.settings 2 | 3 | data class Settings( 4 | val isOnboardingDone: Boolean, 5 | val theme: Theme, 6 | val colorPalette: ColorPalettes, 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/model/settings/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data.model.settings 2 | 3 | import androidx.annotation.StringRes 4 | import com.looker.kenko.R 5 | import com.looker.kenko.ui.theme.colorSchemes.ColorSchemes 6 | import com.looker.kenko.ui.theme.colorSchemes.defaultColorSchemes 7 | import com.looker.kenko.ui.theme.colorSchemes.sereneColorSchemes 8 | import com.looker.kenko.ui.theme.colorSchemes.twilightColorSchemes 9 | import com.looker.kenko.ui.theme.colorSchemes.zestfulColorSchemes 10 | 11 | enum class Theme(@StringRes val nameRes: Int) { 12 | System(R.string.label_theme_system), 13 | Light(R.string.label_theme_light), 14 | Dark(R.string.label_theme_dark), 15 | } 16 | 17 | enum class ColorPalettes(val scheme: ColorSchemes?) { 18 | Dynamic(null), 19 | Default(defaultColorSchemes), 20 | Zestful(zestfulColorSchemes), 21 | Serene(sereneColorSchemes), 22 | Twilight(twilightColorSchemes), 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/repository/ExerciseRepo.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data.repository 2 | 3 | import com.looker.kenko.data.model.Exercise 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface ExerciseRepo { 7 | 8 | val stream: Flow> 9 | 10 | val numberOfExercise: Flow 11 | 12 | suspend fun get(id: Int): Exercise? 13 | 14 | suspend fun upsert(exercise: Exercise) 15 | 16 | suspend fun remove(id: Int) 17 | 18 | suspend fun isExerciseAvailable(name: String): Boolean 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/repository/PerformanceRepo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.repository 16 | 17 | import androidx.compose.runtime.Immutable 18 | 19 | interface PerformanceRepo { 20 | 21 | suspend fun updateModifiers() 22 | 23 | suspend fun getPerformance(exerciseId: Int?, planId: Int?): Performance? 24 | 25 | } 26 | 27 | @Immutable 28 | class Performance( 29 | val days: IntArray, 30 | val ratings: FloatArray, 31 | ) 32 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/repository/PlanRepo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.repository 16 | 17 | import com.looker.kenko.data.model.Exercise 18 | import com.looker.kenko.data.model.Labels.Difficulty 19 | import com.looker.kenko.data.model.Labels.Equipment 20 | import com.looker.kenko.data.model.Labels.Focus 21 | import com.looker.kenko.data.model.Labels.Time 22 | import com.looker.kenko.data.model.Plan 23 | import com.looker.kenko.data.model.PlanItem 24 | import kotlinx.coroutines.flow.Flow 25 | import kotlinx.datetime.DayOfWeek 26 | 27 | interface PlanRepo { 28 | 29 | val plans: Flow> 30 | 31 | val current: Flow 32 | 33 | val planItems: Flow> 34 | 35 | fun planItems(id: Int): Flow> 36 | 37 | fun planItems(id: Int, day: DayOfWeek): Flow> 38 | 39 | fun activeExercises(day: DayOfWeek): Flow> 40 | 41 | suspend fun plan(id: Int): Plan? 42 | 43 | suspend fun planNameExists(name: String): Boolean 44 | 45 | suspend fun getPlanItems(id: Int): List 46 | 47 | suspend fun getPlanItems(id: Int, day: DayOfWeek): List 48 | 49 | suspend fun createPlan( 50 | name: String, 51 | description: String? = null, 52 | difficulty: Difficulty? = null, 53 | focus: Focus? = null, 54 | equipment: Equipment? = null, 55 | time: Time? = null, 56 | ): Int 57 | 58 | suspend fun updatePlan(plan: Plan) 59 | 60 | suspend fun setCurrent(id: Int) 61 | 62 | suspend fun deletePlan(id: Int) 63 | 64 | suspend fun addItem(planItem: PlanItem) 65 | 66 | suspend fun removeItem(id: Long) 67 | 68 | suspend fun removeItemById(exerciseId: Int) 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/repository/SessionRepo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025. LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.repository 16 | 17 | import com.looker.kenko.data.local.model.SetType 18 | import com.looker.kenko.data.model.Session 19 | import com.looker.kenko.data.model.Set 20 | import kotlinx.coroutines.flow.Flow 21 | import kotlinx.datetime.LocalDate 22 | 23 | interface SessionRepo { 24 | 25 | val stream: Flow> 26 | 27 | val setsCount: Flow 28 | 29 | suspend fun addSet(sessionId: Int, set: Set) 30 | 31 | suspend fun addSet(sessionId: Int, exerciseId: Int, weight: Float, reps: Int, setType: SetType) 32 | 33 | suspend fun removeSet(setId: Int) 34 | 35 | suspend fun getSessionIdOrCreate(date: LocalDate): Int 36 | 37 | fun streamByDate(date: LocalDate): Flow 38 | 39 | suspend fun getSets(sessionId: Int): List 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/repository/SettingsRepo.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data.repository 2 | 3 | import com.looker.kenko.data.model.settings.ColorPalettes 4 | import com.looker.kenko.data.model.settings.Settings 5 | import com.looker.kenko.data.model.settings.Theme 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface SettingsRepo { 9 | 10 | val stream: Flow 11 | 12 | fun get(block: Settings.() -> T): Flow 13 | 14 | suspend fun setOnboardingDone() 15 | 16 | suspend fun setColorPalette(colorPalette: ColorPalettes) 17 | 18 | suspend fun setTheme(theme: Theme) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/repository/local/LocalExerciseRepo.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.data.repository.local 2 | 3 | import com.looker.kenko.data.local.dao.ExerciseDao 4 | import com.looker.kenko.data.local.model.ExerciseEntity 5 | import com.looker.kenko.data.local.model.toEntity 6 | import com.looker.kenko.data.local.model.toExternal 7 | import com.looker.kenko.data.model.Exercise 8 | import com.looker.kenko.data.repository.ExerciseRepo 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.map 11 | import javax.inject.Inject 12 | 13 | class LocalExerciseRepo @Inject constructor( 14 | private val dao: ExerciseDao, 15 | ) : ExerciseRepo { 16 | 17 | override val stream: Flow> = 18 | dao.stream().map { it.map(ExerciseEntity::toExternal) } 19 | 20 | override val numberOfExercise: Flow = dao.number() 21 | 22 | override suspend fun get(id: Int): Exercise? = 23 | dao.get(id)?.toExternal() 24 | 25 | override suspend fun upsert(exercise: Exercise) { 26 | dao.upsert(exercise.toEntity()) 27 | } 28 | 29 | override suspend fun remove(id: Int) { 30 | dao.delete(id) 31 | } 32 | 33 | override suspend fun isExerciseAvailable(name: String): Boolean = 34 | dao.exists(name) 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/data/repository/local/LocalPerformanceRepo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.data.repository.local 16 | 17 | import com.looker.kenko.data.local.dao.PerformanceDao 18 | import com.looker.kenko.data.local.model.defaultSetTypes 19 | import com.looker.kenko.data.repository.Performance 20 | import com.looker.kenko.data.repository.PerformanceRepo 21 | import javax.inject.Inject 22 | 23 | class LocalPerformanceRepo @Inject constructor( 24 | private val performanceDao: PerformanceDao, 25 | ) : PerformanceRepo { 26 | 27 | override suspend fun updateModifiers() { 28 | performanceDao.upsertSetTypeLookup(defaultSetTypes()) 29 | } 30 | 31 | override suspend fun getPerformance(exerciseId: Int?, planId: Int?): Performance? = 32 | performanceDao.getPerformance(exerciseId, planId) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.SupervisorJob 11 | import javax.inject.Qualifier 12 | import javax.inject.Singleton 13 | 14 | @Retention(AnnotationRetention.RUNTIME) 15 | @Qualifier 16 | annotation class IoDispatcher 17 | 18 | @Retention(AnnotationRetention.RUNTIME) 19 | @Qualifier 20 | annotation class DefaultDispatcher 21 | 22 | @Retention(AnnotationRetention.RUNTIME) 23 | @Qualifier 24 | annotation class ApplicationScope 25 | 26 | @Module 27 | @InstallIn(SingletonComponent::class) 28 | object AppModule { 29 | 30 | @Provides 31 | @IoDispatcher 32 | fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO 33 | 34 | @Provides 35 | @DefaultDispatcher 36 | fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default 37 | 38 | @Provides 39 | @Singleton 40 | @ApplicationScope 41 | fun providesCoroutineScope( 42 | @DefaultDispatcher dispatcher: CoroutineDispatcher, 43 | ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.di 16 | 17 | import android.content.Context 18 | import com.looker.kenko.data.local.KenkoDatabase 19 | import com.looker.kenko.data.local.dao.ExerciseDao 20 | import com.looker.kenko.data.local.dao.PerformanceDao 21 | import com.looker.kenko.data.local.dao.PlanDao 22 | import com.looker.kenko.data.local.dao.PlanHistoryDao 23 | import com.looker.kenko.data.local.dao.SessionDao 24 | import com.looker.kenko.data.local.dao.SetsDao 25 | import com.looker.kenko.data.local.kenkoDatabase 26 | import dagger.Module 27 | import dagger.Provides 28 | import dagger.hilt.InstallIn 29 | import dagger.hilt.android.qualifiers.ApplicationContext 30 | import dagger.hilt.components.SingletonComponent 31 | import javax.inject.Singleton 32 | 33 | @Module 34 | @InstallIn(SingletonComponent::class) 35 | object DatabaseModule { 36 | 37 | @Provides 38 | @Singleton 39 | fun provideDatabase( 40 | @ApplicationContext context: Context, 41 | ): KenkoDatabase = kenkoDatabase(context) 42 | 43 | @Provides 44 | @Singleton 45 | fun provideExerciseDao( 46 | database: KenkoDatabase, 47 | ): ExerciseDao = database.exerciseDao() 48 | 49 | @Provides 50 | @Singleton 51 | fun provideSessionDao( 52 | database: KenkoDatabase, 53 | ): SessionDao = database.sessionDao() 54 | 55 | @Provides 56 | @Singleton 57 | fun providePlanDao( 58 | database: KenkoDatabase, 59 | ): PlanDao = database.planDao() 60 | 61 | @Provides 62 | @Singleton 63 | fun provideSetsDao( 64 | database: KenkoDatabase, 65 | ): SetsDao = database.setsDao() 66 | 67 | @Provides 68 | @Singleton 69 | fun providePlanHistoryDao( 70 | database: KenkoDatabase, 71 | ): PlanHistoryDao = database.historyDao() 72 | 73 | @Provides 74 | @Singleton 75 | fun providePerformanceDao( 76 | database: KenkoDatabase, 77 | ): PerformanceDao = database.performanceDao() 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/di/DatastoreModule.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.di 2 | 3 | import android.content.Context 4 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory 5 | import androidx.datastore.preferences.preferencesDataStoreFile 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.qualifiers.ApplicationContext 10 | import dagger.hilt.components.SingletonComponent 11 | import javax.inject.Singleton 12 | 13 | private const val DATASTORE_FILE_NAME = "settings" 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object DatastoreModule { 18 | 19 | @Provides 20 | @Singleton 21 | fun provideDatastore( 22 | @ApplicationContext context: Context, 23 | ) = PreferenceDataStoreFactory.create( 24 | produceFile = { context.preferencesDataStoreFile(DATASTORE_FILE_NAME) } 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/di/HandlersModule.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.di 2 | 3 | import android.content.Context 4 | import androidx.compose.ui.platform.UriHandler 5 | import com.looker.kenko.data.KenkoUriHandler 6 | import com.looker.kenko.data.StringHandler 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.android.components.ViewModelComponent 11 | import dagger.hilt.android.qualifiers.ApplicationContext 12 | import dagger.hilt.android.scopes.ViewModelScoped 13 | 14 | @Module 15 | @InstallIn(ViewModelComponent::class) 16 | object HandlersModule { 17 | 18 | @Provides 19 | @ViewModelScoped 20 | fun provideContext( 21 | @ApplicationContext context: Context 22 | ): Context = context 23 | 24 | @Provides 25 | @ViewModelScoped 26 | fun provideUriHandler( 27 | @ApplicationContext context: Context 28 | ): UriHandler = KenkoUriHandler(context) 29 | 30 | @Provides 31 | @ViewModelScoped 32 | fun provideStringHandler( 33 | @ApplicationContext context: Context 34 | ): StringHandler = StringHandler(context) 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.di 2 | 3 | import com.looker.kenko.data.local.datastore.DatastoreSettingsRepo 4 | import com.looker.kenko.data.repository.ExerciseRepo 5 | import com.looker.kenko.data.repository.PerformanceRepo 6 | import com.looker.kenko.data.repository.PlanRepo 7 | import com.looker.kenko.data.repository.SessionRepo 8 | import com.looker.kenko.data.repository.SettingsRepo 9 | import com.looker.kenko.data.repository.local.LocalExerciseRepo 10 | import com.looker.kenko.data.repository.local.LocalPerformanceRepo 11 | import com.looker.kenko.data.repository.local.LocalPlanRepo 12 | import com.looker.kenko.data.repository.local.LocalSessionRepo 13 | import dagger.Binds 14 | import dagger.Module 15 | import dagger.hilt.InstallIn 16 | import dagger.hilt.components.SingletonComponent 17 | 18 | @Module 19 | @InstallIn(SingletonComponent::class) 20 | abstract class RepositoryModule { 21 | 22 | @Binds 23 | abstract fun bindSessionRepo( 24 | repo: LocalSessionRepo, 25 | ): SessionRepo 26 | 27 | @Binds 28 | abstract fun bindPlanRepo( 29 | repo: LocalPlanRepo, 30 | ): PlanRepo 31 | 32 | @Binds 33 | abstract fun bindExerciseRepo( 34 | repo: LocalExerciseRepo, 35 | ): ExerciseRepo 36 | 37 | @Binds 38 | abstract fun bindPerformanceRepo( 39 | repo: LocalPerformanceRepo, 40 | ): PerformanceRepo 41 | 42 | @Binds 43 | abstract fun bindSettingsRepo( 44 | repo: DatastoreSettingsRepo, 45 | ): SettingsRepo 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/KenkoAppState.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.remember 6 | import androidx.navigation.NavController 7 | import androidx.navigation.NavDestination 8 | import androidx.navigation.NavDestination.Companion.hasRoute 9 | import androidx.navigation.NavGraph.Companion.findStartDestination 10 | import androidx.navigation.compose.currentBackStackEntryAsState 11 | import androidx.navigation.compose.rememberNavController 12 | import androidx.navigation.navOptions 13 | import com.looker.kenko.ui.home.navigation.HomeRoute 14 | import com.looker.kenko.ui.navigation.TopLevelDestinations 15 | import com.looker.kenko.ui.navigation.TopLevelDestinations.Home 16 | import com.looker.kenko.ui.navigation.TopLevelDestinations.Performance 17 | import com.looker.kenko.ui.navigation.TopLevelDestinations.Profile 18 | import com.looker.kenko.ui.performance.navigation.PerformanceRoute 19 | import com.looker.kenko.ui.profile.navigation.ProfileRoute 20 | 21 | @Composable 22 | fun rememberKenkoAppState( 23 | navController: NavController = rememberNavController(), 24 | ): KenkoAppState = remember(navController) { 25 | KenkoAppState(navController) 26 | } 27 | 28 | @Stable 29 | class KenkoAppState( 30 | val navController: NavController, 31 | ) { 32 | 33 | private val currentDestination: NavDestination? 34 | @Composable get() = navController.currentBackStackEntryAsState().value?.destination 35 | 36 | val currentTopLevelDestination: TopLevelDestinations? 37 | @Composable get() = when { 38 | currentDestination == null -> null 39 | currentDestination?.hasRoute(HomeRoute::class) == true -> Home 40 | currentDestination?.hasRoute(ProfileRoute::class) == true -> Profile 41 | currentDestination?.hasRoute(PerformanceRoute::class) == true -> Performance 42 | else -> null 43 | } 44 | 45 | val isTopLevelDestination: Boolean 46 | @Composable get() = currentTopLevelDestination != null 47 | 48 | val topLevelDestinations: List = TopLevelDestinations.entries 49 | 50 | fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestinations) { 51 | val route = topLevelDestination.route 52 | if (navController.currentDestination?.hasRoute(route::class) == true) return 53 | val topLevelNavOptions = navOptions { 54 | popUpTo(navController.graph.findStartDestination().id) { 55 | saveState = true 56 | } 57 | launchSingleTop = true 58 | restoreState = true 59 | } 60 | navController.navigate(route = route, navOptions = topLevelNavOptions) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui 16 | 17 | import android.content.Context 18 | import androidx.lifecycle.ViewModel 19 | import androidx.lifecycle.viewModelScope 20 | import com.looker.kenko.data.model.settings.Theme 21 | import com.looker.kenko.data.repository.PerformanceRepo 22 | import com.looker.kenko.data.repository.SettingsRepo 23 | import com.looker.kenko.ui.theme.colorSchemes.ColorSchemes 24 | import com.looker.kenko.ui.theme.colorSchemes.zestfulColorSchemes 25 | import com.looker.kenko.ui.theme.dynamicColorSchemes 26 | import com.looker.kenko.utils.asStateFlow 27 | import dagger.hilt.android.lifecycle.HiltViewModel 28 | import kotlinx.coroutines.flow.StateFlow 29 | import kotlinx.coroutines.flow.first 30 | import kotlinx.coroutines.flow.map 31 | import kotlinx.coroutines.launch 32 | import kotlinx.coroutines.runBlocking 33 | import javax.inject.Inject 34 | 35 | @HiltViewModel 36 | class MainViewModel @Inject constructor( 37 | repo: SettingsRepo, 38 | performanceRepo: PerformanceRepo, 39 | context: Context, 40 | ) : ViewModel() { 41 | 42 | val theme: StateFlow = repo.get { theme } 43 | .asStateFlow(Theme.System) 44 | 45 | val colorScheme: StateFlow = repo.get { colorPalette } 46 | .map { it.scheme ?: dynamicColorSchemes(context) ?: zestfulColorSchemes } 47 | .asStateFlow(zestfulColorSchemes) 48 | 49 | val isOnboardingDone: Boolean = runBlocking { repo.get { isOnboardingDone }.first() } 50 | 51 | init { 52 | viewModelScope.launch { 53 | performanceRepo.updateModifiers() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/addEditExercise/navigation/AddEditExerciseNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.addEditExercise.navigation 2 | 3 | import androidx.navigation.NavController 4 | import androidx.navigation.NavGraphBuilder 5 | import androidx.navigation.NavOptions 6 | import androidx.navigation.compose.composable 7 | import com.looker.kenko.data.model.MuscleGroups 8 | import com.looker.kenko.ui.addEditExercise.AddEditExercise 9 | import kotlinx.serialization.Serializable 10 | 11 | @Serializable 12 | data class AddEditExerciseRoute( 13 | val id: Int? = null, 14 | val target: String? = null, 15 | ) 16 | 17 | fun NavController.navigateToAddEditExercise( 18 | id: Int? = null, 19 | target: MuscleGroups? = null, 20 | navOptions: NavOptions? = null, 21 | ) { 22 | navigate(AddEditExerciseRoute(id, target?.name), navOptions) 23 | } 24 | 25 | fun NavGraphBuilder.addEditExercise( 26 | onBackPress: () -> Unit, 27 | ) { 28 | composable { 29 | AddEditExercise(onBackPress, onBackPress) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/Border.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.components 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.unit.Dp 7 | import androidx.compose.ui.unit.dp 8 | 9 | val KenkoBorderWidth: Dp = 1.4.dp 10 | 11 | val PrimaryBorder: BorderStroke 12 | @Composable 13 | get() = BorderStroke(KenkoBorderWidth, MaterialTheme.colorScheme.primary) 14 | 15 | val SecondaryBorder: BorderStroke 16 | @Composable 17 | get() = BorderStroke(KenkoBorderWidth, MaterialTheme.colorScheme.secondary) 18 | 19 | val OutlineBorder: BorderStroke 20 | @Composable 21 | get() = BorderStroke(KenkoBorderWidth, MaterialTheme.colorScheme.secondary) 22 | 23 | val OnSurfaceBorder: BorderStroke 24 | @Composable 25 | get() = BorderStroke(KenkoBorderWidth, MaterialTheme.colorScheme.onSurface) 26 | 27 | val OnSurfaceVariantBorder: BorderStroke 28 | @Composable 29 | get() = BorderStroke(KenkoBorderWidth, MaterialTheme.colorScheme.onSurfaceVariant) 30 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/Days.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.components 2 | 3 | import androidx.compose.animation.animateColor 4 | import androidx.compose.animation.core.animateDp 5 | import androidx.compose.animation.core.updateTransition 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.lazy.LazyRow 12 | import androidx.compose.foundation.lazy.items 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material3.LocalContentColor 15 | import androidx.compose.material3.LocalTextStyle 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.CompositionLocalProvider 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.drawBehind 23 | import androidx.compose.ui.graphics.graphicsLayer 24 | import androidx.compose.ui.unit.dp 25 | import kotlinx.datetime.DayOfWeek 26 | 27 | @Composable 28 | fun HorizontalDaySelector( 29 | item: @Composable (DayOfWeek) -> Unit, 30 | modifier: Modifier = Modifier, 31 | ) { 32 | LazyRow( 33 | modifier = modifier.fillMaxWidth(), 34 | horizontalArrangement = Arrangement.Center, 35 | ) { 36 | items(DayOfWeek.entries) { 37 | item(it) 38 | } 39 | } 40 | } 41 | 42 | @Composable 43 | fun DaySelectorChip( 44 | selected: Boolean, 45 | onClick: () -> Unit, 46 | modifier: Modifier = Modifier, 47 | content: @Composable () -> Unit, 48 | ) { 49 | val transition = updateTransition(selected, label = "Day Selector") 50 | val background by transition.animateColor(label = "Color") { 51 | if (it) { 52 | MaterialTheme.colorScheme.secondaryContainer 53 | } else { 54 | MaterialTheme.colorScheme.surfaceContainer 55 | } 56 | } 57 | val corner by transition.animateDp(label = "Corner") { 58 | if (it) { 59 | 28.dp 60 | } else { 61 | 12.dp 62 | } 63 | } 64 | Box( 65 | modifier = Modifier 66 | .padding(horizontal = 4.dp) 67 | .graphicsLayer { 68 | clip = true 69 | shape = RoundedCornerShape(corner) 70 | } 71 | .drawBehind { 72 | drawRect(color = background) 73 | } 74 | .clickable(onClick = onClick) 75 | .padding(vertical = 8.dp, horizontal = 12.dp) 76 | .then(modifier), 77 | contentAlignment = Alignment.Center, 78 | ) { 79 | CompositionLocalProvider( 80 | LocalTextStyle provides MaterialTheme.typography.labelLarge, 81 | LocalContentColor provides MaterialTheme.colorScheme.onSurface, 82 | content = content, 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/Dp.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025. LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui.components 16 | 17 | import androidx.compose.runtime.Immutable 18 | import androidx.compose.runtime.Stable 19 | import androidx.compose.ui.unit.Density 20 | import androidx.compose.ui.unit.Dp 21 | import androidx.compose.ui.util.packFloats 22 | import androidx.compose.ui.util.unpackFloat1 23 | import androidx.compose.ui.util.unpackFloat2 24 | 25 | @Immutable 26 | @JvmInline 27 | value class DpRange(private val packedFloat: Long) { 28 | 29 | @Stable 30 | val start: Dp get() = Dp(unpackFloat1(packedFloat)) 31 | 32 | @Stable 33 | val end: Dp get() = Dp(unpackFloat2(packedFloat)) 34 | 35 | } 36 | 37 | 38 | operator fun Dp.rangeTo(other: Dp): DpRange = DpRange(packFloats(value, other.value)) 39 | 40 | context(Density) 41 | operator fun DpRange.contains(other: Float): Boolean = other > start.toPx() && other < end.toPx() 42 | 43 | operator fun DpRange.contains(other: Dp): Boolean = other > start && other < end 44 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/Labels.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.res.stringArrayResource 11 | import androidx.compose.ui.text.TextStyle 12 | import androidx.compose.ui.unit.dp 13 | import com.looker.kenko.R 14 | 15 | @Composable 16 | fun LiftingQuotes( 17 | modifier: Modifier = Modifier, 18 | color: Color = MaterialTheme.colorScheme.onSurfaceVariant, 19 | style: TextStyle = MaterialTheme.typography.labelSmall, 20 | ) { 21 | val array = stringArrayResource(R.array.label_lifting_quotes) 22 | val randomQuote = remember { 23 | array.random() 24 | } 25 | Text( 26 | text = randomQuote, 27 | style = style, 28 | color = color, 29 | modifier = Modifier 30 | .padding(top = 8.dp, bottom = 6.dp) 31 | .then(modifier), 32 | ) 33 | } 34 | 35 | @Composable 36 | fun HealthQuotes( 37 | modifier: Modifier = Modifier, 38 | color: Color = MaterialTheme.colorScheme.onSurfaceVariant, 39 | style: TextStyle = MaterialTheme.typography.labelSmall, 40 | ) { 41 | val array = stringArrayResource(R.array.label_health_quotes) 42 | val randomQuote = remember { 43 | array.random() 44 | } 45 | Text( 46 | text = randomQuote, 47 | style = style, 48 | color = color, 49 | modifier = Modifier 50 | .padding(top = 8.dp, bottom = 6.dp) 51 | .then(modifier), 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/List.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui.components 16 | 17 | import androidx.compose.foundation.layout.Spacer 18 | import androidx.compose.foundation.layout.height 19 | import androidx.compose.foundation.lazy.LazyListScope 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.unit.Dp 22 | import androidx.compose.ui.unit.dp 23 | 24 | private val FAB_PADDING = 88.dp 25 | 26 | fun LazyListScope.endItem( 27 | height: Dp = FAB_PADDING, 28 | ) { 29 | item { 30 | Spacer(Modifier.height(height)) 31 | // TODO: Add some item here to make list end look good behind FAB 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/ReferenceItem.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025. LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui.components 16 | 17 | import androidx.compose.foundation.layout.Row 18 | import androidx.compose.foundation.layout.Spacer 19 | import androidx.compose.foundation.layout.padding 20 | import androidx.compose.foundation.layout.width 21 | import androidx.compose.material3.FilledTonalIconButton 22 | import androidx.compose.material3.Icon 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Surface 25 | import androidx.compose.material3.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.res.stringResource 30 | import androidx.compose.ui.tooling.preview.Preview 31 | import androidx.compose.ui.unit.dp 32 | import com.looker.kenko.R 33 | import com.looker.kenko.ui.theme.KenkoIcons 34 | import com.looker.kenko.ui.theme.KenkoTheme 35 | 36 | @Composable 37 | fun ReferenceItem( 38 | onClick: () -> Unit, 39 | modifier: Modifier = Modifier, 40 | ) { 41 | Surface( 42 | modifier = modifier, 43 | shape = MaterialTheme.shapes.extraLarge, 44 | color = MaterialTheme.colorScheme.surfaceContainer, 45 | onClick = onClick 46 | ) { 47 | Row( 48 | modifier = Modifier.padding(16.dp), 49 | verticalAlignment = Alignment.CenterVertically 50 | ) { 51 | Icon(painter = KenkoIcons.Lightbulb, contentDescription = null) 52 | Spacer(modifier = Modifier.width(8.dp)) 53 | Text( 54 | text = stringResource(R.string.label_reference), 55 | style = MaterialTheme.typography.titleMedium 56 | ) 57 | Spacer(modifier = Modifier.weight(1F)) 58 | FilledTonalIconButton(onClick = onClick) { 59 | Icon(painter = KenkoIcons.ArrowOutward, contentDescription = null) 60 | } 61 | } 62 | } 63 | } 64 | 65 | @Preview 66 | @Composable 67 | private fun ReferenceItemPreview() { 68 | KenkoTheme { 69 | ReferenceItem(onClick = {}) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/Snackbar.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.components 2 | 3 | import androidx.compose.foundation.layout.heightIn 4 | import androidx.compose.foundation.shape.CircleShape 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Snackbar 7 | import androidx.compose.material3.SnackbarData 8 | import androidx.compose.material3.SnackbarDuration 9 | import androidx.compose.material3.SnackbarVisuals 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.tooling.preview.PreviewLightDark 13 | import androidx.compose.ui.unit.dp 14 | import com.looker.kenko.ui.theme.KenkoTheme 15 | 16 | @Composable 17 | fun ErrorSnackbar( 18 | data: SnackbarData, 19 | modifier: Modifier = Modifier, 20 | ) { 21 | Snackbar( 22 | modifier = modifier.heightIn(84.dp), 23 | shape = CircleShape, 24 | containerColor = MaterialTheme.colorScheme.error, 25 | contentColor = MaterialTheme.colorScheme.onError, 26 | dismissActionContentColor = MaterialTheme.colorScheme.onError, 27 | snackbarData = data, 28 | ) 29 | } 30 | 31 | @PreviewLightDark 32 | @Composable 33 | private fun SnackbarPreview() { 34 | KenkoTheme { 35 | ErrorSnackbar( 36 | data = object : SnackbarData { 37 | override val visuals: SnackbarVisuals 38 | get() = object : SnackbarVisuals { 39 | override val actionLabel: String? 40 | get() = null 41 | override val duration: SnackbarDuration 42 | get() = SnackbarDuration.Long 43 | override val message: String 44 | get() = "Error" 45 | override val withDismissAction: Boolean 46 | get() = false 47 | } 48 | 49 | override fun dismiss() {} 50 | 51 | override fun performAction() {} 52 | } 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/TextField.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.text.input.TextFieldDecorator 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Text 7 | import androidx.compose.material3.TextFieldColors 8 | import androidx.compose.material3.TextFieldDefaults 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.graphics.Color 12 | 13 | fun kenkoTextDecorator(supportingText: String) = TextFieldDecorator { 14 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 15 | Text( 16 | text = supportingText, 17 | style = MaterialTheme.typography.labelSmall, 18 | color = MaterialTheme.colorScheme.outline 19 | ) 20 | it() 21 | } 22 | } 23 | 24 | @Composable 25 | fun kenkoTextFieldColor(): TextFieldColors = TextFieldDefaults.colors( 26 | disabledIndicatorColor = Color.Transparent, 27 | errorIndicatorColor = Color.Transparent, 28 | focusedIndicatorColor = Color.Transparent, 29 | unfocusedIndicatorColor = Color.Transparent, 30 | errorContainerColor = MaterialTheme.colorScheme.errorContainer 31 | ) 32 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/icons/AddLarge.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.components.icons 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.NonZero 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap 7 | import androidx.compose.ui.graphics.StrokeJoin 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.unit.dp 11 | 12 | val AddLarge: ImageVector 13 | get() { 14 | if (_addLarge != null) { 15 | return _addLarge!! 16 | } 17 | _addLarge = icon( 18 | name = "AddLarge", 19 | viewPort = 960.0F to 960.0F, 20 | size = 120.dp to 120.dp, 21 | ) { 22 | path( 23 | fill = null, 24 | stroke = SolidColor(Color.Black), 25 | strokeLineWidth = 12.0f, 26 | strokeLineCap = StrokeCap.Round, 27 | strokeLineJoin = StrokeJoin.Round, 28 | strokeLineMiter = 0.0f, 29 | pathFillType = NonZero 30 | ) { 31 | moveTo(450.0f, 510.0f) 32 | lineTo(250.0f, 510.0f) 33 | quadTo(237.25f, 510.0f, 228.63f, 501.37f) 34 | quadTo(220.0f, 492.74f, 220.0f, 479.99f) 35 | quadTo(220.0f, 467.23f, 228.63f, 458.62f) 36 | quadTo(237.25f, 450.0f, 250.0f, 450.0f) 37 | lineTo(450.0f, 450.0f) 38 | lineTo(450.0f, 250.0f) 39 | quadTo(450.0f, 237.25f, 458.63f, 228.63f) 40 | quadTo(467.26f, 220.0f, 480.01f, 220.0f) 41 | quadTo(492.77f, 220.0f, 501.38f, 228.63f) 42 | quadTo(510.0f, 237.25f, 510.0f, 250.0f) 43 | lineTo(510.0f, 450.0f) 44 | lineTo(710.0f, 450.0f) 45 | quadTo(722.75f, 450.0f, 731.37f, 458.63f) 46 | quadTo(740.0f, 467.26f, 740.0f, 480.01f) 47 | quadTo(740.0f, 492.77f, 731.37f, 501.38f) 48 | quadTo(722.75f, 510.0f, 710.0f, 510.0f) 49 | lineTo(510.0f, 510.0f) 50 | lineTo(510.0f, 710.0f) 51 | quadTo(510.0f, 722.75f, 501.37f, 731.37f) 52 | quadTo(492.74f, 740.0f, 479.99f, 740.0f) 53 | quadTo(467.23f, 740.0f, 458.62f, 731.37f) 54 | quadTo(450.0f, 722.75f, 450.0f, 710.0f) 55 | lineTo(450.0f, 510.0f) 56 | close() 57 | } 58 | 59 | } 60 | return _addLarge!! 61 | } 62 | 63 | private var _addLarge: ImageVector? = null 64 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/icons/ArrowOutwardLarge.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.components.icons 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.NonZero 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap 7 | import androidx.compose.ui.graphics.StrokeJoin 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.unit.dp 11 | 12 | val ArrowOutwardLarge: ImageVector 13 | get() { 14 | if (_arrowOutwardLarge != null) { 15 | return _arrowOutwardLarge!! 16 | } 17 | _arrowOutwardLarge = icon( 18 | name = "ArrowOutwardLarge", 19 | viewPort = 960.0F to 960.0F, 20 | size = 120.dp to 120.dp, 21 | ) { 22 | path( 23 | fill = null, 24 | stroke = SolidColor(Color.Black), 25 | strokeLineWidth = 12.0f, 26 | strokeLineCap = StrokeCap.Round, 27 | strokeLineJoin = StrokeJoin.Round, 28 | strokeLineMiter = 0.0f, 29 | pathFillType = NonZero 30 | ) { 31 | moveTo(645.77f, 312.15f) 32 | lineTo(272.46f, 685.08f) 33 | quadTo(264.15f, 693.38f, 251.58f, 693.19f) 34 | quadTo(239.0f, 693.0f, 230.69f, 684.69f) 35 | quadTo(222.39f, 676.38f, 222.39f, 664.0f) 36 | quadTo(222.39f, 651.62f, 230.69f, 643.31f) 37 | lineTo(603.62f, 270.0f) 38 | lineTo(275.77f, 270.0f) 39 | quadTo(263.02f, 270.0f, 254.39f, 261.37f) 40 | quadTo(245.77f, 252.74f, 245.77f, 239.99f) 41 | quadTo(245.77f, 227.23f, 254.39f, 218.62f) 42 | quadTo(263.02f, 210.0f, 275.77f, 210.0f) 43 | lineTo(669.61f, 210.0f) 44 | quadTo(684.98f, 210.0f, 695.37f, 220.39f) 45 | quadTo(705.77f, 230.79f, 705.77f, 246.15f) 46 | lineTo(705.77f, 640.0f) 47 | quadTo(705.77f, 652.75f, 697.14f, 661.37f) 48 | quadTo(688.51f, 670.0f, 675.76f, 670.0f) 49 | quadTo(663.0f, 670.0f, 654.38f, 661.37f) 50 | quadTo(645.77f, 652.75f, 645.77f, 640.0f) 51 | lineTo(645.77f, 312.15f) 52 | close() 53 | } 54 | } 55 | return _arrowOutwardLarge!! 56 | } 57 | 58 | private var _arrowOutwardLarge: ImageVector? = null 59 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/icons/Dawn.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.components.icons 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.NonZero 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap.Companion.Butt 7 | import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.unit.dp 11 | 12 | val Dawn: ImageVector 13 | get() { 14 | if (_dawn != null) { 15 | return _dawn!! 16 | } 17 | _dawn = icon( 18 | name = "Dawn", 19 | viewPort = 92F to 92F, 20 | size = 164.dp to 164.dp, 21 | ) { 22 | path( 23 | fill = SolidColor(Color.Black), stroke = SolidColor(Color.Black), 24 | strokeLineWidth = 3.0f, strokeLineCap = Butt, strokeLineJoin = Miter, 25 | strokeLineMiter = 4.0f, pathFillType = NonZero 26 | ) { 27 | moveTo(46.0f, 0.0f) 28 | lineTo(46.0089f, 45.9547f) 29 | lineTo(63.6035f, 3.5015f) 30 | lineTo(46.0256f, 45.9618f) 31 | lineTo(78.5268f, 13.4731f) 32 | lineTo(46.0382f, 45.9744f) 33 | lineTo(88.4984f, 28.3965f) 34 | lineTo(46.0453f, 45.9911f) 35 | lineTo(92.0f, 46.0f) 36 | lineTo(46.0453f, 46.0089f) 37 | lineTo(88.4984f, 63.6035f) 38 | lineTo(46.0382f, 46.0256f) 39 | lineTo(78.5268f, 78.5268f) 40 | lineTo(46.0256f, 46.0382f) 41 | lineTo(63.6035f, 88.4984f) 42 | lineTo(46.0089f, 46.0453f) 43 | lineTo(46.0f, 92.0f) 44 | lineTo(45.9911f, 46.0453f) 45 | lineTo(28.3965f, 88.4984f) 46 | lineTo(45.9744f, 46.0382f) 47 | lineTo(13.4731f, 78.5268f) 48 | lineTo(45.9618f, 46.0256f) 49 | lineTo(3.5015f, 63.6035f) 50 | lineTo(45.9547f, 46.0089f) 51 | lineTo(0.0f, 46.0f) 52 | lineTo(45.9547f, 45.9911f) 53 | lineTo(3.5015f, 28.3965f) 54 | lineTo(45.9618f, 45.9744f) 55 | lineTo(13.4731f, 13.4731f) 56 | lineTo(45.9744f, 45.9618f) 57 | lineTo(28.3965f, 3.5015f) 58 | lineTo(45.9911f, 45.9547f) 59 | lineTo(46.0f, 0.0f) 60 | close() 61 | } 62 | } 63 | return _dawn!! 64 | } 65 | 66 | private var _dawn: ImageVector? = null 67 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/icons/Helper.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.components.icons 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | import androidx.compose.ui.unit.Dp 5 | import androidx.compose.ui.unit.dp 6 | 7 | fun icon( 8 | name: String, 9 | viewPort: Pair, 10 | size: Pair = viewPort.first.dp to viewPort.second.dp, 11 | autoMirror: Boolean = false, 12 | block: ImageVector.Builder.() -> ImageVector.Builder, 13 | ): ImageVector { 14 | return ImageVector.Builder( 15 | name = name, 16 | defaultWidth = size.first, 17 | defaultHeight = size.second, 18 | viewportWidth = viewPort.first, 19 | viewportHeight = viewPort.second, 20 | autoMirror = autoMirror 21 | ).block().build() 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/components/icons/QuarterCircles.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.components.icons 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.NonZero 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap.Companion.Butt 7 | import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.unit.dp 11 | 12 | val QuarterCircles: ImageVector 13 | get() { 14 | if (_quarterCircles != null) { 15 | return _quarterCircles!! 16 | } 17 | _quarterCircles = icon( 18 | name = "QuarterCircles", 19 | viewPort = 246F to 311F, 20 | size = 180.dp to 228.dp, 21 | ) { 22 | path( 23 | fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, 24 | strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, 25 | pathFillType = NonZero 26 | ) { 27 | moveTo(247.924f, 192.73f) 28 | lineTo(247.136f, 63.733f) 29 | lineTo(118.115f, 65.16f) 30 | curveTo(118.348f, 99.402f, 131.87f, 130.272f, 153.731f, 153.174f) 31 | lineTo(25.528f, 154.57f) 32 | curveTo(26.027f, 225.81f, 84.129f, 282.901f, 155.424f, 282.141f) 33 | lineTo(154.533f, 154.004f) 34 | curveTo(178.177f, 178.225f, 211.308f, 193.116f, 247.924f, 192.73f) 35 | close() 36 | } 37 | } 38 | return _quarterCircles!! 39 | } 40 | 41 | private var _quarterCircles: ImageVector? = null 42 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/exercises/ExercisesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.exercises 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material3.SnackbarHostState 5 | import androidx.compose.runtime.Stable 6 | import androidx.compose.ui.platform.UriHandler 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import com.looker.kenko.R 10 | import com.looker.kenko.data.StringHandler 11 | import com.looker.kenko.data.model.Exercise 12 | import com.looker.kenko.data.model.MuscleGroups 13 | import com.looker.kenko.data.repository.ExerciseRepo 14 | import com.looker.kenko.utils.asStateFlow 15 | import dagger.hilt.android.lifecycle.HiltViewModel 16 | import kotlinx.coroutines.flow.Flow 17 | import kotlinx.coroutines.flow.MutableStateFlow 18 | import kotlinx.coroutines.flow.StateFlow 19 | import kotlinx.coroutines.flow.combine 20 | import kotlinx.coroutines.launch 21 | import javax.inject.Inject 22 | 23 | @HiltViewModel 24 | class ExercisesViewModel @Inject constructor( 25 | private val repo: ExerciseRepo, 26 | private val uriHandler: UriHandler, 27 | private val stringHandler: StringHandler, 28 | ) : ViewModel() { 29 | 30 | // null -> all 31 | private val selectedTarget: MutableStateFlow = MutableStateFlow(null) 32 | 33 | private val exercisesStream: Flow> = repo.stream 34 | 35 | val snackbarState = SnackbarHostState() 36 | 37 | val exercises: StateFlow = combine( 38 | exercisesStream, 39 | selectedTarget, 40 | ) { exercises, target -> 41 | val selectedExercises = if (target == null) { 42 | exercises 43 | } else { 44 | exercises.filter { it.target == target } 45 | } 46 | ExercisesUiState( 47 | exercises = selectedExercises, 48 | selected = target, 49 | ) 50 | }.asStateFlow(ExercisesUiState()) 51 | 52 | fun removeExercise(id: Int?) { 53 | viewModelScope.launch { 54 | if (id == null) { 55 | snackbarState.showSnackbar(stringHandler.getString(R.string.error_unknown)) 56 | return@launch 57 | } 58 | repo.remove(id) 59 | } 60 | } 61 | 62 | fun setTarget(value: MuscleGroups?) { 63 | viewModelScope.launch { 64 | selectedTarget.emit(value) 65 | } 66 | } 67 | 68 | fun onReferenceClick(reference: String) { 69 | viewModelScope.launch { 70 | try { 71 | uriHandler.openUri(reference) 72 | } catch (e: IllegalStateException) { 73 | snackbarState.showSnackbar( 74 | e.message ?: stringHandler.getString(R.string.error_invalid_url) 75 | ) 76 | } 77 | } 78 | } 79 | } 80 | 81 | val MuscleGroups?.string: Int 82 | @StringRes 83 | get() = this?.stringRes ?: R.string.label_all_muscle_groups 84 | 85 | @Stable 86 | class ExercisesUiState( 87 | val exercises: List = emptyList(), 88 | val selected: MuscleGroups? = null, 89 | ) 90 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/exercises/navigation/ExercisesNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.exercises.navigation 2 | 3 | import androidx.hilt.navigation.compose.hiltViewModel 4 | import androidx.navigation.NavController 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.NavOptions 7 | import androidx.navigation.compose.composable 8 | import com.looker.kenko.data.model.MuscleGroups 9 | import com.looker.kenko.ui.exercises.Exercises 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | object ExercisesRoute 14 | 15 | fun NavController.navigateToExercises(navOptions: NavOptions? = null) { 16 | navigate(ExercisesRoute, navOptions = navOptions) 17 | } 18 | 19 | fun NavGraphBuilder.exercises( 20 | onExerciseClick: (id: Int?) -> Unit, 21 | onCreateClick: (target: MuscleGroups?) -> Unit, 22 | onBackPress: () -> Unit, 23 | ) { 24 | composable { 25 | Exercises( 26 | onExerciseClick = onExerciseClick, 27 | onCreateClick = onCreateClick, 28 | onBackPress = onBackPress, 29 | viewModel = hiltViewModel(), 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/extensions/Modifier.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.extensions 2 | 3 | import androidx.compose.ui.Modifier 4 | import androidx.compose.ui.draw.rotate 5 | import androidx.compose.ui.layout.layout 6 | 7 | fun Modifier.vertical(towardsRight: Boolean = true) = 8 | layout { measurable, constraints -> 9 | val placeable = measurable.measure(constraints) 10 | layout(placeable.height, placeable.width) { 11 | placeable.place( 12 | x = -(placeable.width / 2 - placeable.height / 2), 13 | y = -(placeable.height / 2 - placeable.width / 2) 14 | ) 15 | } 16 | }.rotate(90F * (if (towardsRight) 1 else -1)) 17 | 18 | const val PHI = 16F / 10F 19 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/extensions/PaddingValues.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.extensions 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.calculateEndPadding 5 | import androidx.compose.foundation.layout.calculateStartPadding 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.platform.LocalLayoutDirection 8 | import androidx.compose.ui.unit.LayoutDirection 9 | 10 | @Composable 11 | operator fun PaddingValues.plus(other: PaddingValues): PaddingValues { 12 | val layoutDirection: LayoutDirection = LocalLayoutDirection.current 13 | return PaddingValues( 14 | start = calculateStartPadding(layoutDirection) + other.calculateStartPadding(layoutDirection), 15 | end = calculateEndPadding(layoutDirection) + other.calculateEndPadding(layoutDirection), 16 | top = calculateTopPadding() + other.calculateTopPadding(), 17 | bottom = calculateBottomPadding() + other.calculateBottomPadding() 18 | ) 19 | } 20 | 21 | @Composable 22 | operator fun PaddingValues.minus(other: PaddingValues): PaddingValues { 23 | val layoutDirection: LayoutDirection = LocalLayoutDirection.current 24 | return PaddingValues( 25 | start = calculateStartPadding(layoutDirection) - other.calculateStartPadding(layoutDirection), 26 | end = calculateEndPadding(layoutDirection) - other.calculateEndPadding(layoutDirection), 27 | top = calculateTopPadding() - other.calculateTopPadding(), 28 | bottom = calculateBottomPadding() - other.calculateBottomPadding() 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/extensions/String.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.extensions 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | 6 | @Composable 7 | fun normalizeInt(value: Int, padding: Char = '0', length: Int = 2): String { 8 | return remember(value) { 9 | value.toString().padStart(length, padding) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/getStarted/GetStartedOldViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025. LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui.getStarted 16 | 17 | import androidx.lifecycle.SavedStateHandle 18 | import androidx.lifecycle.ViewModel 19 | import androidx.navigation.toRoute 20 | import com.looker.kenko.ui.getStarted.navigation.GetStartedRoute 21 | import dagger.hilt.android.lifecycle.HiltViewModel 22 | import javax.inject.Inject 23 | 24 | @HiltViewModel 25 | class GetStartedOldViewModel @Inject constructor( 26 | savedStateHandle: SavedStateHandle, 27 | ) : ViewModel() { 28 | 29 | private val routeData: GetStartedRoute = savedStateHandle.toRoute() 30 | val isOnboardingDone = routeData.isOnboardingDone 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/getStarted/navigation/GetStartedNavigation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 LooKeR & Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . 3 | * 4 | */ 5 | 6 | package com.looker.kenko.ui.getStarted.navigation 7 | 8 | import androidx.navigation.NavGraphBuilder 9 | import androidx.navigation.compose.composable 10 | import com.looker.kenko.ui.getStarted.GetStartedOld 11 | import kotlinx.serialization.Serializable 12 | 13 | @Serializable 14 | data class GetStartedRoute(val isOnboardingDone: Boolean) 15 | 16 | fun NavGraphBuilder.getStarted(onNext: () -> Unit) { 17 | composable { 18 | GetStartedOld(onNext) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025. LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui.home 16 | 17 | import androidx.compose.runtime.Immutable 18 | import androidx.lifecycle.ViewModel 19 | import com.looker.kenko.data.model.localDate 20 | import com.looker.kenko.data.repository.PlanRepo 21 | import com.looker.kenko.data.repository.SessionRepo 22 | import com.looker.kenko.utils.asStateFlow 23 | import dagger.hilt.android.lifecycle.HiltViewModel 24 | import kotlinx.coroutines.flow.combine 25 | import javax.inject.Inject 26 | 27 | @HiltViewModel 28 | class HomeViewModel @Inject constructor( 29 | planRepo: PlanRepo, 30 | sessionRepo: SessionRepo, 31 | ) : ViewModel() { 32 | 33 | private val planStream = planRepo.current 34 | 35 | private val sessionStream = sessionRepo.streamByDate(localDate) 36 | 37 | private val sessionsStream = sessionRepo.stream 38 | 39 | private val planItemStream = planRepo.planItems 40 | 41 | val state = combine( 42 | planStream, 43 | sessionStream, 44 | sessionsStream, 45 | planItemStream, 46 | ) { currentPlan, currentSession, sessions, planItems -> 47 | val isFirstSession = sessions.size <= 1 && sessions.firstOrNull()?.date == localDate 48 | HomeUiData( 49 | isPlanSelected = currentPlan != null, 50 | isSessionStarted = currentSession != null, 51 | isTodayEmpty = planItems.isEmpty(), 52 | isFirstSession = isFirstSession, 53 | currentPlanId = currentPlan?.id, 54 | ) 55 | }.asStateFlow( 56 | HomeUiData( 57 | isPlanSelected = true, 58 | isSessionStarted = false, 59 | isTodayEmpty = false, 60 | isFirstSession = false, 61 | currentPlanId = null, 62 | ), 63 | ) 64 | } 65 | 66 | @Immutable 67 | data class HomeUiData( 68 | val isPlanSelected: Boolean, 69 | val isSessionStarted: Boolean, 70 | val isTodayEmpty: Boolean, 71 | val isFirstSession: Boolean, 72 | val currentPlanId: Int?, 73 | ) 74 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/home/navigation/HomeNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.home.navigation 2 | 3 | import androidx.hilt.navigation.compose.hiltViewModel 4 | import androidx.navigation.NavController 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.NavOptions 7 | import androidx.navigation.compose.composable 8 | import com.looker.kenko.ui.home.Home 9 | import kotlinx.serialization.Serializable 10 | 11 | @Serializable 12 | object HomeRoute 13 | 14 | fun NavController.navigateToHome(navOptions: NavOptions? = null) { 15 | navigate(HomeRoute, navOptions = navOptions) 16 | } 17 | 18 | fun NavGraphBuilder.home( 19 | onSelectPlanClick: () -> Unit, 20 | onAddExerciseClick: () -> Unit, 21 | onExploreSessionsClick: () -> Unit, 22 | onExploreExercisesClick: () -> Unit, 23 | onStartSessionClick: () -> Unit, 24 | onCurrentPlanClick: (Int) -> Unit, 25 | ) { 26 | composable { 27 | Home( 28 | onSelectPlanClick = onSelectPlanClick, 29 | onAddExerciseClick = onAddExerciseClick, 30 | onExploreSessionsClick = onExploreSessionsClick, 31 | onExploreExercisesClick = onExploreExercisesClick, 32 | onStartSessionClick = onStartSessionClick, 33 | onCurrentPlanClick = onCurrentPlanClick, 34 | viewModel = hiltViewModel(), 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/navigation/TopLevelDestinations.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025. LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui.navigation 16 | 17 | import androidx.annotation.DrawableRes 18 | import androidx.annotation.StringRes 19 | import com.looker.kenko.R 20 | import com.looker.kenko.ui.home.navigation.HomeRoute 21 | import com.looker.kenko.ui.performance.navigation.PerformanceRoute 22 | import com.looker.kenko.ui.profile.navigation.ProfileRoute 23 | 24 | // Manually add 80.dp padding for bottom app bar 25 | enum class TopLevelDestinations( 26 | @StringRes val labelRes: Int, 27 | @DrawableRes val icon: Int, 28 | val route: Any, 29 | ) { 30 | Performance(R.string.label_performance, R.drawable.ic_show_chart, PerformanceRoute), 31 | Home(R.string.label_home, R.drawable.ic_home, HomeRoute), 32 | Profile(R.string.label_profile, R.drawable.ic_radio_button_unchecked, ProfileRoute), 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/performance/Performance.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui.performance 16 | 17 | import androidx.compose.foundation.layout.Box 18 | import androidx.compose.foundation.layout.fillMaxSize 19 | import androidx.compose.foundation.layout.size 20 | import androidx.compose.material3.CircularProgressIndicator 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.tooling.preview.Preview 26 | import androidx.compose.ui.unit.dp 27 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 28 | import com.looker.kenko.data.repository.Performance 29 | import com.looker.kenko.ui.theme.KenkoTheme 30 | 31 | private val graphSizeModifier = Modifier.size( 32 | width = 400.dp, 33 | height = 200.dp, 34 | ) 35 | 36 | @Composable 37 | fun Performance( 38 | viewModel: PerformanceViewModel, 39 | onAddNewExercise: () -> Unit, 40 | ) { 41 | val state by viewModel.state.collectAsStateWithLifecycle() 42 | when (state) { 43 | PerformanceStateError.NoValidPerformance -> { 44 | 45 | } 46 | 47 | PerformanceStateError.NotEnoughData -> { 48 | 49 | } 50 | 51 | PerformanceUiState.Loading -> { 52 | Box( 53 | modifier = Modifier.fillMaxSize(), 54 | contentAlignment = Alignment.Center, 55 | ) { 56 | CircularProgressIndicator() 57 | } 58 | } 59 | 60 | is PerformanceUiState.Success -> { 61 | val data = (state as PerformanceUiState.Success).data 62 | PerformancePlot(data.performance) 63 | } 64 | } 65 | } 66 | 67 | @Composable 68 | private fun PerformancePlot( 69 | performance: Performance, 70 | modifier: Modifier = Modifier, 71 | ) { 72 | } 73 | 74 | @Preview(showBackground = true) 75 | @Composable 76 | private fun PerformancePlotPreview() { 77 | KenkoTheme { 78 | PerformancePlot( 79 | performance = Performance( 80 | days = intArrayOf(1, 2, 3, 4, 5), 81 | ratings = floatArrayOf(1.5F, 2F, 4F, 2.5F, 3F), 82 | ), 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/performance/components/LineGraph.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui.performance.components 16 | 17 | import androidx.compose.foundation.background 18 | import androidx.compose.foundation.layout.Spacer 19 | import androidx.compose.foundation.layout.padding 20 | import androidx.compose.foundation.layout.size 21 | import androidx.compose.material3.MaterialTheme 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.drawBehind 25 | import androidx.compose.ui.tooling.preview.Preview 26 | import androidx.compose.ui.unit.dp 27 | import com.looker.kenko.ui.theme.KenkoTheme 28 | 29 | @Composable 30 | fun LineGraph( 31 | xAxis: IntArray, 32 | yAxis: FloatArray, 33 | modifier: Modifier = Modifier, 34 | ) { 35 | val data = rememberPoints(points = yAxis) {} 36 | Spacer( 37 | modifier = modifier 38 | .background( 39 | color = MaterialTheme.colorScheme.surfaceContainer, 40 | shape = MaterialTheme.shapes.large, 41 | ) 42 | .padding(12.dp) 43 | .drawBehind { 44 | drawPath(data.toPath(size), color = data.lineColor, style = data.stroke) 45 | }, 46 | ) 47 | } 48 | 49 | @Preview 50 | @Composable 51 | private fun LineGraphPreview() { 52 | KenkoTheme { 53 | LineGraph( 54 | xAxis = intArrayOf(1, 2, 3, 4, 5), 55 | yAxis = floatArrayOf(22F, 13.7F, 31F, 20F, 22F), 56 | modifier = Modifier.size(400.dp, 300.dp), 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/performance/components/Point.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | @file:Suppress("NOTHING_TO_INLINE") 16 | 17 | package com.looker.kenko.ui.performance.components 18 | 19 | import androidx.compose.runtime.Immutable 20 | import androidx.compose.runtime.Stable 21 | import androidx.compose.ui.geometry.Offset 22 | import androidx.compose.ui.geometry.Size 23 | import androidx.compose.ui.util.packFloats 24 | import androidx.compose.ui.util.unpackFloat1 25 | import androidx.compose.ui.util.unpackFloat2 26 | 27 | @Stable 28 | fun Point(x: Float, y: Float) = Point(packFloats(x, y)) 29 | 30 | @Immutable 31 | @JvmInline 32 | value class Point(val packedValue: Long) { 33 | 34 | @Stable 35 | val x: Float get() = unpackFloat1(packedValue) 36 | 37 | @Stable 38 | val y: Float get() = unpackFloat2(packedValue) 39 | 40 | @Stable 41 | inline operator fun component1(): Float = x 42 | 43 | @Stable 44 | inline operator fun component2(): Float = y 45 | 46 | override fun toString(): String = "Point: ($x, $y)" 47 | 48 | } 49 | 50 | @Stable 51 | fun Offset.toCartesian(size: Size): Point = Point(x = x, y = size.height - y) 52 | 53 | @Stable 54 | fun Point.toOffset(size: Size): Offset = Offset(x = x, y = size.height - y) 55 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/performance/components/Points.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui.performance.components 16 | 17 | import androidx.compose.material3.LocalContentColor 18 | import androidx.compose.material3.LocalTextStyle 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.CompositionLocalProvider 22 | import androidx.compose.runtime.Immutable 23 | import androidx.compose.ui.geometry.Size 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.graphics.Path 26 | import androidx.compose.ui.graphics.StrokeCap 27 | import androidx.compose.ui.graphics.StrokeJoin 28 | import androidx.compose.ui.graphics.drawscope.Stroke 29 | 30 | @Immutable 31 | class Points( 32 | val points: FloatArray, 33 | val stroke: Stroke, 34 | val smoothen: Boolean, 35 | val pointColor: Color, 36 | val lineColor: Color, 37 | val gridColor: Color, 38 | val label: @Composable () -> Unit, 39 | ) 40 | 41 | fun Points.toPath(size: Size): Path = Path().apply { 42 | if (points.isEmpty()) return@apply 43 | val stepSize = size.width / points.size 44 | val xScaleSize = size.height / (points.max() - points.min()) 45 | moveTo(points[0], 0F) 46 | for (i in 1..points.lastIndex) { 47 | val x = points[i] * 20F 48 | val y = stepSize * i 49 | lineTo(x, y) 50 | } 51 | } 52 | 53 | private val defaultStroke = Stroke( 54 | width = 1F, 55 | cap = StrokeCap.Round, 56 | join = StrokeJoin.Round, 57 | ) 58 | 59 | @Composable 60 | fun rememberPoints( 61 | points: FloatArray, 62 | stroke: Stroke = defaultStroke, 63 | smoothen: Boolean = true, 64 | pointColor: Color = MaterialTheme.colorScheme.secondary, 65 | lineColor: Color = MaterialTheme.colorScheme.secondaryContainer, 66 | gridColor: Color = MaterialTheme.colorScheme.outlineVariant, 67 | labelColor: Color = MaterialTheme.colorScheme.outline, 68 | label: @Composable () -> Unit, 69 | ): Points = Points( 70 | points = points, 71 | stroke = stroke, 72 | smoothen = smoothen, 73 | pointColor = pointColor, 74 | lineColor = lineColor, 75 | gridColor = gridColor, 76 | label = { 77 | CompositionLocalProvider( 78 | LocalTextStyle provides MaterialTheme.typography.labelMedium, 79 | LocalContentColor provides labelColor, 80 | content = label, 81 | ) 82 | }, 83 | ) 84 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/performance/navigation/PerformanceNavigation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui.performance.navigation 16 | 17 | import androidx.hilt.navigation.compose.hiltViewModel 18 | import androidx.navigation.NavController 19 | import androidx.navigation.NavGraphBuilder 20 | import androidx.navigation.NavOptions 21 | import androidx.navigation.compose.composable 22 | import com.looker.kenko.ui.performance.Performance 23 | import kotlinx.serialization.Serializable 24 | 25 | @Serializable 26 | object PerformanceRoute 27 | 28 | fun NavController.navigateToPerformance(navOptions: NavOptions? = null) { 29 | navigate(PerformanceRoute, navOptions) 30 | } 31 | 32 | fun NavGraphBuilder.performance( 33 | onAddNewExercise: () -> Unit, 34 | ) { 35 | composable { 36 | Performance( 37 | viewModel = hiltViewModel(), 38 | onAddNewExercise = onAddNewExercise, 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/planEdit/navigation/PlanEditNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.planEdit.navigation 2 | 3 | import androidx.hilt.navigation.compose.hiltViewModel 4 | import androidx.navigation.NavController 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.NavOptions 7 | import androidx.navigation.compose.composable 8 | import com.looker.kenko.ui.planEdit.PlanEdit 9 | import kotlinx.serialization.Serializable 10 | 11 | @Serializable 12 | data class PlanEditRoute( 13 | val id: Int, 14 | ) 15 | 16 | fun NavController.navigateToPlanEdit(id: Int = -1, navOptions: NavOptions? = null) { 17 | navigate(PlanEditRoute(id), navOptions) 18 | } 19 | 20 | fun NavGraphBuilder.planEdit( 21 | onBackPress: () -> Unit, 22 | onAddNewExerciseClick: () -> Unit, 23 | ) { 24 | composable { 25 | PlanEdit( 26 | onBackPress = onBackPress, 27 | onAddNewExerciseClick = onAddNewExerciseClick, 28 | viewModel = hiltViewModel() 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/plans/PlanViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.plans 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.looker.kenko.data.model.Plan 6 | import com.looker.kenko.data.repository.PlanRepo 7 | import com.looker.kenko.data.repository.SettingsRepo 8 | import com.looker.kenko.utils.asStateFlow 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.first 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class PlanViewModel @Inject constructor( 16 | private val repo: PlanRepo, 17 | private val settingsRepo: SettingsRepo, 18 | ) : ViewModel() { 19 | 20 | val plans = repo.plans.asStateFlow(emptyList()) 21 | 22 | fun removePlan(id: Int) { 23 | viewModelScope.launch { 24 | repo.deletePlan(id) 25 | } 26 | } 27 | 28 | fun switchPlan(plan: Plan) { 29 | viewModelScope.launch { 30 | if (!plan.isActive) { 31 | repo.setCurrent(plan.id!!) 32 | } else { 33 | repo.updatePlan(plan.copy(isActive = false)) 34 | } 35 | if (repo.current.first() != null) { 36 | settingsRepo.setOnboardingDone() 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/plans/navigation/PlanNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.plans.navigation 2 | 3 | import androidx.hilt.navigation.compose.hiltViewModel 4 | import androidx.navigation.NavController 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.NavOptions 7 | import androidx.navigation.compose.composable 8 | import com.looker.kenko.ui.plans.Plan 9 | import kotlinx.serialization.Serializable 10 | 11 | @Serializable 12 | object PlanRoute 13 | 14 | fun NavController.navigateToPlans(navOptions: NavOptions? = null) { 15 | navigate(PlanRoute, navOptions) 16 | } 17 | 18 | fun NavGraphBuilder.plans( 19 | onPlanClick: (Int) -> Unit, 20 | onBackPress: () -> Unit, 21 | ) { 22 | composable { 23 | Plan( 24 | onPlanClick = onPlanClick, 25 | onBackPress = onBackPress, 26 | viewModel = hiltViewModel(), 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/profile/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.profile 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.lifecycle.ViewModel 5 | import com.looker.kenko.data.model.Plan 6 | import com.looker.kenko.data.model.PlanStat 7 | import com.looker.kenko.data.repository.ExerciseRepo 8 | import com.looker.kenko.data.repository.PlanRepo 9 | import com.looker.kenko.data.repository.SessionRepo 10 | import com.looker.kenko.utils.asStateFlow 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.StateFlow 14 | import kotlinx.coroutines.flow.combine 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class ProfileViewModel @Inject constructor( 19 | planRepo: PlanRepo, 20 | sessionRepo: SessionRepo, 21 | exerciseRepo: ExerciseRepo, 22 | ) : ViewModel() { 23 | 24 | private val currentPlan: Flow = planRepo.current 25 | 26 | val state: StateFlow = combine( 27 | currentPlan, 28 | sessionRepo.setsCount, 29 | exerciseRepo.numberOfExercise, 30 | ) { plan, sets, number -> 31 | ProfileUiState( 32 | numberOfExercises = number, 33 | totalLifts = sets, 34 | isPlanAvailable = plan != null, 35 | planName = plan?.name ?: "", 36 | planStat = plan?.stat, 37 | ) 38 | } 39 | .asStateFlow( 40 | ProfileUiState( 41 | numberOfExercises = 0, 42 | isPlanAvailable = false, 43 | planName = "", 44 | totalLifts = 0, 45 | planStat = null, 46 | ), 47 | ) 48 | } 49 | 50 | @Stable 51 | data class ProfileUiState( 52 | val numberOfExercises: Int, 53 | val isPlanAvailable: Boolean, 54 | val planName: String, 55 | val totalLifts: Int, 56 | val planStat: PlanStat?, 57 | ) 58 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/profile/navigation/ProfileNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.profile.navigation 2 | 3 | import androidx.hilt.navigation.compose.hiltViewModel 4 | import androidx.navigation.NavController 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.NavOptions 7 | import androidx.navigation.compose.composable 8 | import com.looker.kenko.ui.profile.Profile 9 | import kotlinx.serialization.Serializable 10 | 11 | @Serializable 12 | object ProfileRoute 13 | 14 | fun NavController.navigateToProfile(navOptions: NavOptions? = null) { 15 | navigate(ProfileRoute, navOptions) 16 | } 17 | 18 | fun NavGraphBuilder.profile( 19 | onExercisesClick: () -> Unit, 20 | onAddExerciseClick: () -> Unit, 21 | onPlanClick: () -> Unit, 22 | onSettingsClick: () -> Unit, 23 | ) { 24 | composable { 25 | Profile( 26 | onExercisesClick = onExercisesClick, 27 | onAddExerciseClick = onAddExerciseClick, 28 | onPlanClick = onPlanClick, 29 | onSettingsClick = onSettingsClick, 30 | viewModel = hiltViewModel(), 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/selectExercise/SelectExerciseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.selectExercise 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.runtime.snapshotFlow 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.viewModelScope 10 | import com.looker.kenko.data.model.Exercise 11 | import com.looker.kenko.data.model.MuscleGroups 12 | import com.looker.kenko.data.repository.ExerciseRepo 13 | import com.looker.kenko.utils.asStateFlow 14 | import dagger.hilt.android.lifecycle.HiltViewModel 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.StateFlow 17 | import kotlinx.coroutines.flow.asStateFlow 18 | import kotlinx.coroutines.flow.combine 19 | import kotlinx.coroutines.launch 20 | import javax.inject.Inject 21 | 22 | @HiltViewModel 23 | class SelectExerciseViewModel @Inject constructor( 24 | repo: ExerciseRepo, 25 | ) : ViewModel() { 26 | 27 | var searchQuery: String by mutableStateOf("") 28 | private set 29 | 30 | private val searchQueryFlow = snapshotFlow { searchQuery } 31 | 32 | private val exerciseStream = repo.stream 33 | 34 | private val _targetMuscle: MutableStateFlow = MutableStateFlow(null) 35 | val targetMuscle: StateFlow = _targetMuscle.asStateFlow() 36 | 37 | val searchResult = combine( 38 | searchQueryFlow, 39 | targetMuscle, 40 | exerciseStream, 41 | ) { query, target, exercises -> 42 | val filteredExercises = exercises 43 | .filter { 44 | (it.target == target || target == null) && it.satisfiesSearch(query) 45 | } 46 | if (filteredExercises.isNotEmpty()) { 47 | SearchResult.Success(filteredExercises) 48 | } else { 49 | SearchResult.NotFound 50 | } 51 | }.asStateFlow(SearchResult.Loading) 52 | 53 | fun setTarget(target: MuscleGroups?) { 54 | viewModelScope.launch { 55 | _targetMuscle.emit(target) 56 | } 57 | } 58 | 59 | fun setSearch(value: String) { 60 | searchQuery = value 61 | } 62 | 63 | private fun Exercise.satisfiesSearch(query: String): Boolean { 64 | return query.isBlank() || name.contains(query, ignoreCase = true) 65 | } 66 | } 67 | 68 | @Stable 69 | sealed interface SearchResult { 70 | 71 | @Stable 72 | data object Loading : SearchResult 73 | 74 | @Stable 75 | data class Success(val exercises: List) : SearchResult 76 | 77 | @Stable 78 | data object NotFound : SearchResult 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/sessionDetail/navigation/SessionDetailNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.sessionDetail.navigation 2 | 3 | import androidx.hilt.navigation.compose.hiltViewModel 4 | import androidx.navigation.NavController 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.NavOptions 7 | import androidx.navigation.compose.composable 8 | import com.looker.kenko.ui.sessionDetail.SessionDetails 9 | import kotlinx.datetime.LocalDate 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class SessionDetailRoute( 14 | val epochDays: Int, 15 | ) 16 | 17 | fun NavController.navigateToSessionDetail(date: LocalDate?, navOptions: NavOptions? = null) { 18 | navigate(SessionDetailRoute(date?.toEpochDays() ?: -1), navOptions) 19 | } 20 | 21 | fun NavGraphBuilder.sessionDetail( 22 | onBackPress: () -> Unit, 23 | onHistoryClick: (LocalDate) -> Unit, 24 | ) { 25 | composable { 26 | SessionDetails( 27 | onBackPress = onBackPress, 28 | onHistoryClick = onHistoryClick, 29 | viewModel = hiltViewModel(), 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/sessions/SessionsViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.ui.sessions 16 | 17 | import androidx.compose.runtime.Stable 18 | import androidx.lifecycle.ViewModel 19 | import com.looker.kenko.data.model.Session 20 | import com.looker.kenko.data.model.localDate 21 | import com.looker.kenko.data.repository.SessionRepo 22 | import com.looker.kenko.utils.asStateFlow 23 | import dagger.hilt.android.lifecycle.HiltViewModel 24 | import kotlinx.coroutines.flow.Flow 25 | import kotlinx.coroutines.flow.StateFlow 26 | import kotlinx.coroutines.flow.combine 27 | import kotlinx.coroutines.flow.map 28 | import javax.inject.Inject 29 | 30 | @HiltViewModel 31 | class SessionsViewModel @Inject constructor( 32 | repo: SessionRepo, 33 | ) : ViewModel() { 34 | private val sessionsStream: Flow> = repo.stream.map { it.asReversed() } 35 | 36 | private val isCurrentSessionActive: Flow = repo.streamByDate(localDate).map { it != null } 37 | 38 | val state: StateFlow = combine( 39 | sessionsStream, 40 | isCurrentSessionActive, 41 | ) { sessions, isCurrentSessionActive -> 42 | SessionsUiData( 43 | sessions = sessions, 44 | isCurrentSessionActive = isCurrentSessionActive, 45 | ) 46 | }.asStateFlow(SessionsUiData(emptyList(), false)) 47 | } 48 | 49 | @Stable 50 | data class SessionsUiData( 51 | val sessions: List, 52 | val isCurrentSessionActive: Boolean, 53 | ) 54 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/sessions/navigation/SessionsPageNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.sessions.navigation 2 | 3 | import androidx.hilt.navigation.compose.hiltViewModel 4 | import androidx.navigation.NavController 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.NavOptions 7 | import androidx.navigation.compose.composable 8 | import com.looker.kenko.ui.sessions.Sessions 9 | import kotlinx.datetime.LocalDate 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | object SessionRoute 14 | 15 | fun NavController.navigateToSessions(navOptions: NavOptions? = null) { 16 | navigate(SessionRoute, navOptions = navOptions) 17 | } 18 | 19 | fun NavGraphBuilder.sessions( 20 | onSessionClick: (LocalDate?) -> Unit, 21 | onBackPress: () -> Unit, 22 | ) { 23 | composable { 24 | Sessions( 25 | onSessionClick = onSessionClick, 26 | onBackPress = onBackPress, 27 | viewModel = hiltViewModel(), 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/settings/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.settings 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.looker.kenko.data.model.settings.ColorPalettes 7 | import com.looker.kenko.data.model.settings.Theme 8 | import com.looker.kenko.data.repository.SettingsRepo 9 | import com.looker.kenko.utils.asStateFlow 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.flow.StateFlow 12 | import kotlinx.coroutines.flow.combine 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class SettingsViewModel @Inject constructor( 18 | private val repo: SettingsRepo, 19 | ) : ViewModel() { 20 | 21 | val state: StateFlow = combine( 22 | repo.get { theme }, 23 | repo.get { colorPalette } 24 | ) { theme, colorPalette -> 25 | SettingsUiData(theme, colorPalette) 26 | }.asStateFlow(SettingsUiData(Theme.System, ColorPalettes.Default)) 27 | 28 | fun updateTheme(theme: Theme) { 29 | viewModelScope.launch { 30 | repo.setTheme(theme) 31 | } 32 | } 33 | 34 | fun updateColorPalette(colorPalette: ColorPalettes) { 35 | viewModelScope.launch { 36 | repo.setColorPalette(colorPalette) 37 | } 38 | } 39 | } 40 | 41 | @Stable 42 | data class SettingsUiData( 43 | val selectedTheme: Theme, 44 | val selectedColorPalette: ColorPalettes, 45 | ) 46 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/settings/navigation/SettingsNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.settings.navigation 2 | 3 | import androidx.hilt.navigation.compose.hiltViewModel 4 | import androidx.navigation.NavController 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.NavOptions 7 | import androidx.navigation.compose.composable 8 | import com.looker.kenko.ui.settings.Settings 9 | import kotlinx.serialization.Serializable 10 | 11 | @Serializable 12 | object SettingsRoute 13 | 14 | fun NavController.navigateToSettings(navOptions: NavOptions? = null) { 15 | navigate(SettingsRoute, navOptions) 16 | } 17 | 18 | fun NavGraphBuilder.settings( 19 | onBackPress: () -> Unit, 20 | ) { 21 | composable { 22 | Settings( 23 | onBackPress = onBackPress, 24 | viewModel = hiltViewModel(), 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/theme/Shapes.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.theme 2 | 3 | import androidx.compose.foundation.shape.CornerBasedShape 4 | import androidx.compose.foundation.shape.CornerSize 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.Shapes 7 | import androidx.compose.ui.unit.Dp 8 | import androidx.compose.ui.unit.dp 9 | 10 | val Shapes = Shapes( 11 | extraSmall = RoundedCornerShape(4.dp), 12 | small = RoundedCornerShape(8.dp), 13 | medium = RoundedCornerShape(14.dp), 14 | large = RoundedCornerShape(20.dp), 15 | extraLarge = RoundedCornerShape(28.dp), 16 | ) 17 | 18 | fun CornerBasedShape.end( 19 | bottomEnd: Dp = 0.dp, 20 | topEnd: Dp = bottomEnd, 21 | ): CornerBasedShape = 22 | copy(bottomEnd = CornerSize(bottomEnd), topEnd = CornerSize(topEnd)) 23 | 24 | fun CornerBasedShape.start( 25 | bottomStart: Dp = 0.dp, 26 | topStart: Dp = bottomStart, 27 | ): CornerBasedShape = 28 | copy(bottomStart = CornerSize(bottomStart), topStart = CornerSize(topStart)) 29 | 30 | fun CornerBasedShape.end( 31 | end: CornerBasedShape, 32 | ): CornerBasedShape = 33 | copy(bottomEnd = end.bottomEnd, topEnd = end.topEnd) 34 | 35 | fun CornerBasedShape.start( 36 | start: CornerBasedShape, 37 | ): CornerBasedShape = 38 | copy(bottomStart = start.bottomStart, topStart = start.topStart) 39 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 LooKeR & Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . 3 | * 4 | */ 5 | 6 | package com.looker.kenko.ui.theme 7 | 8 | import android.app.Activity 9 | import android.content.Context 10 | import android.os.Build 11 | import android.view.View 12 | import androidx.compose.foundation.isSystemInDarkTheme 13 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 14 | import androidx.compose.material3.MaterialExpressiveTheme 15 | import androidx.compose.material3.dynamicDarkColorScheme 16 | import androidx.compose.material3.dynamicLightColorScheme 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.SideEffect 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.ui.platform.LocalView 21 | import androidx.core.view.WindowCompat 22 | import com.looker.kenko.R 23 | import com.looker.kenko.data.model.settings.Theme 24 | import com.looker.kenko.ui.theme.colorSchemes.ColorSchemes 25 | import com.looker.kenko.ui.theme.colorSchemes.zestfulColorSchemes 26 | 27 | @OptIn(ExperimentalMaterial3ExpressiveApi::class) 28 | @Composable 29 | fun KenkoTheme( 30 | theme: Theme = Theme.System, 31 | colorSchemes: ColorSchemes = zestfulColorSchemes, 32 | content: @Composable () -> Unit, 33 | ) { 34 | val systemTheme = isSystemInDarkTheme() 35 | val isDarkTheme = remember(theme) { 36 | when (theme) { 37 | Theme.System -> systemTheme 38 | Theme.Light -> false 39 | Theme.Dark -> true 40 | } 41 | } 42 | val colorScheme = if (isDarkTheme) { 43 | colorSchemes.dark 44 | } else { 45 | colorSchemes.light 46 | } 47 | 48 | val localView = LocalView.current 49 | SideEffect { setupSystemBar(localView, isDarkTheme) } 50 | 51 | MaterialExpressiveTheme( 52 | colorScheme = colorScheme, 53 | typography = Typography, 54 | shapes = Shapes, 55 | content = content, 56 | ) 57 | } 58 | 59 | fun setupSystemBar(view: View, isDarkTheme: Boolean) { 60 | if (view.isInEditMode) return 61 | val window = (view.context as Activity).window 62 | with(WindowCompat.getInsetsController(window, view)) { 63 | isAppearanceLightStatusBars = !isDarkTheme 64 | isAppearanceLightNavigationBars = !isDarkTheme 65 | } 66 | } 67 | 68 | fun dynamicColorSchemes(context: Context): ColorSchemes? = 69 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 70 | ColorSchemes( 71 | light = dynamicLightColorScheme(context), 72 | dark = dynamicDarkColorScheme(context), 73 | nameRes = R.string.label_color_scheme_dynamic, 74 | ) 75 | } else { 76 | null 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/ui/theme/colorSchemes/ColorSchemes.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.ui.theme.colorSchemes 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material3.ColorScheme 5 | import androidx.compose.runtime.Immutable 6 | 7 | @Immutable 8 | data class ColorSchemes( 9 | val light: ColorScheme, 10 | val dark: ColorScheme, 11 | val mediumContrastLight: ColorScheme? = null, 12 | val mediumContrastDark: ColorScheme? = null, 13 | val highContrastLight: ColorScheme? = null, 14 | val highContrastDark: ColorScheme? = null, 15 | @StringRes val nameRes: Int, 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/utils/Collection.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.utils 16 | 17 | inline fun Collection.sumOf(selector: (T) -> Float): Float { 18 | var sum = -1F 19 | for (element in this) { 20 | sum += selector(element) 21 | } 22 | return sum 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/utils/DateTime.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 LooKeR & Contributors 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * You should have received a copy of the GNU General Public License 12 | * along with this program. If not, see . 13 | */ 14 | 15 | package com.looker.kenko.utils 16 | 17 | import kotlinx.datetime.Clock 18 | import kotlinx.datetime.LocalDate 19 | import kotlinx.datetime.TimeZone 20 | import kotlinx.datetime.daysUntil 21 | import kotlinx.datetime.todayIn 22 | import java.text.SimpleDateFormat 23 | import java.util.Date 24 | import java.util.Locale 25 | import kotlin.time.Duration.Companion.days 26 | 27 | @JvmInline 28 | value class EpochDays(val value: Int) 29 | 30 | operator fun EpochDays.plus(other: EpochDays) = EpochDays(value + other.value) 31 | 32 | fun LocalDate.toLocalEpochDays() = EpochDays(toEpochDays()) 33 | 34 | fun formatDate( 35 | date: LocalDate, 36 | dateTimeFormat: DateTimeFormat = DateTimeFormat.Short, 37 | locale: Locale = Locale.getDefault(Locale.Category.FORMAT), 38 | ): String { 39 | val format = SimpleDateFormat(dateTimeFormat.format, locale) 40 | return format.format( 41 | Date(date.toEpochDays().days.inWholeMilliseconds), 42 | ) 43 | } 44 | 45 | fun formatDate( 46 | epochDays: Int, 47 | dateTimeFormat: DateTimeFormat = DateTimeFormat.Short, 48 | locale: Locale = Locale.getDefault(Locale.Category.FORMAT), 49 | ): String { 50 | val format = SimpleDateFormat(dateTimeFormat.format, locale) 51 | return format.format( 52 | Date(epochDays.days.inWholeMilliseconds), 53 | ) 54 | } 55 | 56 | val LocalDate.isToday: Boolean 57 | get() = daysUntil(Clock.System.todayIn(TimeZone.currentSystemDefault())) == 0 58 | 59 | enum class DateTimeFormat(val format: String) { 60 | Short("dd-MMM"), Long("EEEE, dd-MMM-yyyy"), 61 | Day("EEEE") 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/utils/Url.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.utils 2 | 3 | import android.util.Patterns 4 | 5 | fun String.isValidUrl(): Boolean { 6 | return this.startsWith("http://") || this.startsWith("https://") && Patterns.WEB_URL.matcher(this).matches() 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/looker/kenko/utils/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.looker.kenko.utils 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.SharingStarted 8 | import kotlinx.coroutines.flow.StateFlow 9 | import kotlinx.coroutines.flow.stateIn 10 | 11 | private const val SHARING_STARTED_DEFAULT = 5_000L 12 | 13 | context(ViewModel) 14 | fun Flow.asStateFlow( 15 | initial: T, 16 | coroutineScope: CoroutineScope = viewModelScope, 17 | started: SharingStarted = SharingStarted.WhileSubscribed(SHARING_STARTED_DEFAULT) 18 | ): StateFlow = stateIn( 19 | scope = coroutineScope, 20 | started = started, 21 | initialValue = initial 22 | ) 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_forward.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_outward.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_history.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keyboard_arrow_left.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keyboard_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_lightbulb.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_radio_button_unchecked.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_remove.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_save.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_show_chart.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tactic.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_verified.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/font/darkergrotesque_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/font/darkergrotesque_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/darkergrotesque_semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/font/darkergrotesque_semibold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/spacemono_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/font/spacemono_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/spacemono_normal.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/font/spacemono_normal.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |