├── .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 |

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 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/looker/kenko/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.looker.kenko
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.app) apply false
3 | alias(libs.plugins.kotlin.android) apply false
4 | alias(libs.plugins.kotlinx.serialization) apply false
5 | alias(libs.plugins.compose.compiler) apply false
6 | alias(libs.plugins.ksp) apply false
7 | alias(libs.plugins.room) apply false
8 | alias(libs.plugins.hilt) apply false
9 | }
10 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | org.gradle.jvmargs=-Xmx4096M -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8
3 | android.useAndroidX=true
4 | kotlin.code.style=official
5 | android.nonTransitiveRClass=true
6 | android.enableR8.fullMode=true
7 | org.gradle.caching=true
8 | org.gradle.configuration-cache=true
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
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 | #Sat Oct 05 19:36:47 IST 2024
16 | distributionBase=GRADLE_USER_HOME
17 | distributionPath=wrapper/dists
18 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
19 | zipStoreBase=GRADLE_USER_HOME
20 | zipStorePath=wrapper/dists
21 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/100000.txt:
--------------------------------------------------------------------------------
1 | Initial Release
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/101000.txt:
--------------------------------------------------------------------------------
1 | Added:
2 |
3 | * New Home Page
4 | * Back button on Exercises Page
5 | * Option to open References from workout page(if added)
6 |
7 | Changed:
8 |
9 | * Splash Screen Image to reduce dependency on `NonFreeNet`
10 | * Whole Plan card is clickable
11 |
12 | Fixed:
13 |
14 | * APK dependency tree encryption
15 | * Color of icons on some buttons
16 | * `Zestful` Color Palettes
17 | * Crash when using invalid reference
18 | * UI/UX for Exercises Page
19 | * Some navigation crashes
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/101010.txt:
--------------------------------------------------------------------------------
1 | Fixed:
2 |
3 | * Navigation from home screen
4 | * Annoying animations on home page
5 | * Plan Edit Page
6 | * Back button on all pages
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/102000.txt:
--------------------------------------------------------------------------------
1 | Added:
2 |
3 | * Support for isometric exercises
4 | * Deleting Sets / Exercises / Plans
5 |
6 | Changed:
7 |
8 | * Error message height
9 | * Chips type in `Select Exercise`
10 |
11 | Fixed:
12 |
13 | * Navigation to same page again
14 | * Double back presses
15 | * Swipe gesture on Text field
16 | * Elements squashing on small screens
17 | * Empty exercises
18 | * Invalid reference
19 | * False reference icon
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/103000.txt:
--------------------------------------------------------------------------------
1 | Added
2 | - Drag text field in "Add Set"
3 | - Double tap to edit "set info"
4 | - History Icon (You can check last week's session if it exists)
5 | - Support for Monochrome icon on Android 12+
6 | - Text animation on Onboarding
7 | - Safer way to delete Sets / Exercises / Plans
8 | - New Font for headings
9 |
10 | Changed
11 | - Targets Android 15
12 | - Onboarding screen
13 | - Default theme for new users
14 | - Sorting of muscle groups chips
15 | - Always save plan on going back
16 | - Color in Profile
17 | - Home Screen and On-boarding Screen
18 | - Some buttons and UI elements
19 |
20 | Fixed
21 | - Save button not visible
22 | - Two `Default` theme in Settings
23 | - Scrolling on `Select Exercise` Sheet
24 | - Performance issues on `Add Set` Sheet
25 | - Weird line in the setting wave
26 | - Crash on deleting plan
27 | - On boarding not completing
28 |
29 | Removed
30 | - Gradient in settings
31 |
--------------------------------------------------------------------------------
/metadata/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Kenko is a workout journal which will provide you with appropriate progressive-overload and well thought-out plans
2 |
3 | The app allows you to log your workouts with extraordinary simplicity
4 |
5 | You can create completely personal workout plans, and none of your data will be sent to anybody
6 |
7 | Kenko allows customization of theme with really simple but brutal design
8 |
9 | TODO:
10 |
11 | * Provide progression
12 | * Performance Stats
13 |
--------------------------------------------------------------------------------
/metadata/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/metadata/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/metadata/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/metadata/en-US/images/icon.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/metadata/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/metadata/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/metadata/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/metadata/en-US/images/phoneScreenshots/4.png
--------------------------------------------------------------------------------
/metadata/en-US/images/tvBanner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Iamlooker/Kenko/ea5e39b8fd5b657a3eb46542ac42e4a74e142ab8/metadata/en-US/images/tvBanner.png
--------------------------------------------------------------------------------
/metadata/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | A simple workout journal
2 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | rootProject.name = "Kenko"
17 | include(":app")
18 |
--------------------------------------------------------------------------------