Repository: Iamlooker/Kenko Branch: main Commit: 12625ece57ed Files: 217 Total size: 929.7 KB Directory structure: gitextract_ppp4wail/ ├── .editorconfig ├── .github/ │ └── workflows/ │ ├── prep_release.yml │ └── release.yml ├── .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 │ │ └── 3.json │ └── src/ │ ├── androidTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── looker/ │ │ └── kenko/ │ │ ├── BackupManagerTest.kt │ │ ├── KenkoTestRunner.kt │ │ ├── RepositoryTest.kt │ │ └── RoomDatabaseTesting.kt │ └── main/ │ ├── AndroidManifest.xml │ ├── kotlin/ │ │ └── com/ │ │ └── looker/ │ │ └── kenko/ │ │ ├── KenkoApp.kt │ │ ├── data/ │ │ │ ├── KenkoUriHandler.kt │ │ │ ├── StringHandler.kt │ │ │ ├── backup/ │ │ │ │ ├── BackupManager.kt │ │ │ │ ├── BackupManagerImpl.kt │ │ │ │ └── BackupWorker.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 │ │ │ │ ├── RepsInReserve.kt │ │ │ │ ├── Session.kt │ │ │ │ ├── Set.kt │ │ │ │ └── settings/ │ │ │ │ ├── BackupInterval.kt │ │ │ │ ├── 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 │ │ │ ├── BackupModule.kt │ │ │ ├── DatabaseModule.kt │ │ │ ├── DatastoreModule.kt │ │ │ ├── HandlersModule.kt │ │ │ └── RepositoryModule.kt │ │ ├── ui/ │ │ │ ├── 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 │ │ │ │ ├── EmptyPageIndicator.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 │ │ │ ├── performance/ │ │ │ │ ├── Performance.kt │ │ │ │ ├── PerformanceViewModel.kt │ │ │ │ ├── components/ │ │ │ │ │ ├── Axes.kt │ │ │ │ │ ├── Grid.kt │ │ │ │ │ └── Plot.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/ │ │ ├── DateFormat.kt │ │ ├── DateTime.kt │ │ ├── Path.kt │ │ ├── Url.kt │ │ └── ViewModel.kt │ └── res/ │ ├── drawable/ │ │ ├── ic_add.xml │ │ ├── ic_app_icon.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_person.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 │ ├── mipmap-anydpi/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── values/ │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ ├── values-tr/ │ │ └── strings.xml │ └── xml/ │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle.kts ├── changelog.sh ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── metadata/ │ └── en-US/ │ ├── changelogs/ │ │ ├── 100000.txt │ │ ├── 101000.txt │ │ ├── 101010.txt │ │ ├── 102000.txt │ │ ├── 103000.txt │ │ └── 103020.txt │ ├── full_description.txt │ └── short_description.txt └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true [*.{kt,kts}] indent_size = 4 ij_kotlin_allow_trailing_comma=true ij_kotlin_allow_trailing_comma_on_call_site=true ij_kotlin_name_count_to_use_star_import = 999 ij_kotlin_name_count_to_use_star_import_for_members = 999 [*.{yml,yaml}] indent_size = 2 ================================================ FILE: .github/workflows/prep_release.yml ================================================ name: Prepare Release on: workflow_dispatch: inputs: version: description: 'Version number (e.g., 1.4.0)' required: true type: string jobs: prepare-release: runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v5 - name: Setup Java uses: actions/setup-java@v4 with: java-version: 17 distribution: 'adopt' - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - name: Update versionName in build.gradle.kts run: | VERSION="${{ inputs.version }}" sed -i "s/versionName = \".*\"/versionName = \"$VERSION\"/" app/build.gradle.kts - name: Run fastlaneChangelog task run: ./gradlew fastlaneChangelog - name: Create Pull Request uses: peter-evans/create-pull-request@v7 with: branch: release/${{ inputs.version }} labels: release commit-message: "chore: prepare release ${{ inputs.version }}" title: "chore: release ${{ inputs.version }}" body: | This PR prepares the release for version ${{ inputs.version }}. Changes: - Updated versionName in build.gradle.kts - Updated CHANGELOG.md - Created Fastlane changelog file Once merged, this will trigger the build and publish workflow. ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: pull_request: types: - closed branches: - main jobs: build-and-publish: if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest permissions: contents: write packages: write steps: - name: Checkout repository uses: actions/checkout@v5 - name: Setup Java uses: actions/setup-java@v4 with: java-version: 17 distribution: 'adopt' - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - name: Extract version from branch id: extract_version run: | BRANCH_NAME="${{ github.event.pull_request.head.ref }}" VERSION="${BRANCH_NAME#release/}" echo "tag=$VERSION" >> $GITHUB_OUTPUT - name: Build release APK run: ./gradlew assembleRelease - name: Signing APK uses: r0adkll/sign-android-release@v1 id: sign_apk env: BUILD_TOOLS_VERSION: "36.0.0" with: releaseDirectory: app/build/outputs/apk/release signingKeyBase64: ${{ secrets.KEY_BASE64 }} keyStorePassword: ${{ secrets.STORE_PASS }} alias: ${{ secrets.KEY_ALIAS }} keyPassword: ${{ secrets.KEY_PASS }} - name: Build release AAB run: ./gradlew bundleRelease - uses: r0adkll/sign-android-release@v1 name: Signing AAB id: sign_aab env: BUILD_TOOLS_VERSION: "36.0.0" with: releaseDirectory: app/build/outputs/bundle/release signingKeyBase64: ${{ secrets.KEY_BASE64 }} keyStorePassword: ${{ secrets.STORE_PASS }} alias: ${{ secrets.KEY_ALIAS }} keyPassword: ${{ secrets.KEY_PASS }} - name: Read changelog id: read_changelog run: echo "changelog<> $GITHUB_OUTPUT && bash changelog.sh ${{ steps.extract_version.outputs.tag }} >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT - name: Create release uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.extract_version.outputs.tag }} name: Release ${{ steps.extract_version.outputs.tag }} body: ${{ steps.read_changelog.outputs.changelog }} generate_release_notes: false prerelease: false draft: true files: | ${{steps.sign_apk.outputs.signedReleaseFile}} ${{steps.sign_aab.outputs.signedReleaseFile}} - name: Publish to Play Store uses: r0adkll/upload-google-play@v1 with: packageName: com.looker.kenko track: production releaseName: ${{ steps.extract_version.outputs.tag }} releaseFiles: ${{steps.sign_aab.outputs.signedReleaseFile}} mappingFile: app/build/outputs/mapping/release/mapping.txt debugSymbols: app/build/intermediates/merged_native_libs/release/mergeReleaseNativeLibs/out/lib serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea .DS_Store /build /captures .externalNativeBuild .cxx local.properties *.jks /.kotlin /kls_database.db ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file atleast once a day (if there are any changes). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added - Backup and restore for whole data ## [1.3.2] - 2025-11-13 ### Added - Session History card on Home screen - Set Type selection for sets - Timer which shows time since last set - Monochrome launcher icon on Android 12+ - Allow adding a new exercise directly when it cannot be found in the list - Clean up empty plans from the Plans screen via confirmation dialog - Enable predictive back navigation - Empty state on Sessions screen ### Changed - Removed Bottom navigation bar, added user icon to top bar - Updated Day Switcher component styling for clarity - Hide Lifts card when there are no lifts - Redesigned Session History card and screen - Replaced profile icon on Home screen - Removed "Today" label on Sessions and filtered out empty sessions ### Fixed - Select Plan button alignment - Prevent unintended translation for Turkish app name - Session list now shows most recent first - Lifts card not showing even when lifts existed - Sets from past sessions not shown when the exercise was removed from the corresponding plan ## [1.3.0] - 2025-01-17 ### Added - Drag text field in "Add Set" - Double tap to edit "set info" - History Icon (You can check last week's session if it exists) - Support for Monochrome icon on Android 12+ - Text animation on Onboarding - Safer way to delete Sets / Exercises / Plans - New Font for headings ### Changed - Targets Android 15 - Onboarding screen - Default theme for new users - Sorting of muscle groups chips - Always save plan on going back - Color in Profile - Home Screen and On-boarding Screen - Some buttons and UI elements ### Fixed - Save button not visible - Two `Default` theme in Settings - Scrolling on `Select Exercise` Sheet - Performance issues on `Add Set` Sheet - Weird line in the setting wave - Crash on deleting plan - On boarding not completing - Loads of performance improvements ### Removed - Gradient in settings ## [1.2.0] - 2024-05-26 ### Added - Support for isometric exercises - Deleting Sets / Exercises / Plans ### Changed - Error message height - Chips type in `Select Exercise` ### Fixed - Navigation to same page again - Double back presses - Swipe gesture on reps and weight text field - Elements squashing on small screens - Empty exercises - Invalid reference - False reference icon ## [1.1.1] - 2024-05-19 ### Fixed - Navigation from home screen - Annoying animations on home page - Plan Edit Page - Back button on all pages ## [1.1.0] - 2024-05-19 ### Added - New Home Page - Back button on Exercises Page - Option to open References from workout page(if added) ### Changed - Splash Screen Image to reduce dependency on `NonFreeNet` - Whole Plan card is clickable ### Fixed - APK dependency tree encryption - Color of icons on some buttons - `Zestful` Color Palettes - Crash when using invalid reference - UI/UX for Exercises Page - Some navigation crashes ## [1.0.0] - 2024-05-12 ### Added - Initial Release ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at mohit2002ss@gmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: PRIVACY.md ================================================ **Privacy Policy** 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. 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. This app does not collect or share any sort of data/information of its user. No Internet Permission is asked/requested by the app. 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. **Information Collection and Use** The app does use third-party services that may collect information used to identify you. Link to the privacy policy of third-party service providers used by the app * [Google Play Services](https://www.google.com/policies/privacy/) **Security** 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. **Children’s Privacy** 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. **Changes to This Privacy Policy** 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. This policy is effective as of 2022-05-31 **Contact Us** If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at mohit2002ss@gmail.com. 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/) ================================================ FILE: README.md ================================================
Kenko Kenko is a workout journal which will provide you with appropriate progressive-overload and well thought-out plans
## Screenshots ## CHANGELOGS - Full changelog: [here](https://github.com/Iamlooker/Kenko/blob/main/CHANGELOG.md) - Unreleased changes: [here](https://github.com/Iamlooker/Kenko/blob/main/CHANGELOG.md#unreleased) ## TODO - [x] Add Rating System - [ ] Provide Targeted Overload - [x] Add Import/Export - [x] Add Support for Isometric exercises ## LICENSE ``` Kenko 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 . ```
================================================ FILE: app/.gitignore ================================================ /build /release /reports ================================================ FILE: app/build.gradle.kts ================================================ /* * 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 . */ import com.android.utils.text.dropPrefix import java.time.LocalDate import java.time.ZoneId import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { alias(libs.plugins.android.app) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlinx.serialization) alias(libs.plugins.compose.compiler) alias(libs.plugins.ksp) alias(libs.plugins.room) alias(libs.plugins.hilt) } android { namespace = "com.looker.kenko" compileSdk = 36 defaultConfig { applicationId = "com.looker.kenko" minSdk = 26 targetSdk = 36 versionName = "1.3.2" versionCode = versionCodeFor(versionName) testInstrumentationRunner = "com.looker.kenko.KenkoTestRunner" } room { generateKotlin = true schemaDirectory("$projectDir/schemas") } dependenciesInfo { includeInApk = false } buildTypes { debug { applicationIdSuffix = ".debug" } release { isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 languageVersion = KotlinVersion.KOTLIN_2_2 apiVersion = KotlinVersion.KOTLIN_2_2 freeCompilerArgs.add("-Xcontext-parameters") optIn.add("kotlin.RequiresOptIn") optIn.add("kotlin.time.ExperimentalTime") } } buildFeatures { compose = true buildConfig = true } sourceSets { getByName("androidTest").assets.srcDir("$projectDir/schemas") } lint { disable += "MissingTranslation" } composeCompiler { metricsDestination = file("$projectDir/reports/metrics") reportsDestination = file("$projectDir/reports") } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1,LICENSE*}" excludes += "DebugProbesKt.bin" } } testOptions { unitTests.all { it.useJUnitPlatform() } } } dependencies { implementation(libs.core.ktx) implementation(platform(libs.compose.bom)) implementation(libs.bundles.lifecycle) implementation(libs.activity.compose) implementation(libs.navigation.compose) implementation(libs.savedstate) implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.serialization.json) implementation(libs.bundles.coroutines) implementation(libs.bundles.compose) debugImplementation(libs.bundles.compose.debug) implementation(libs.hilt.android) implementation(libs.hilt.navigation.compose) ksp(libs.hilt.compiler) implementation(libs.datastore) implementation(libs.documentfile) implementation(libs.bundles.work) implementation(libs.bundles.room) ksp(libs.room.compiler) testImplementation(kotlin("test-junit5")) androidTestImplementation(kotlin("test-junit5")) androidTestImplementation(platform(libs.compose.bom)) androidTestImplementation(libs.bundles.instrumented.test) androidTestImplementation(libs.room.test) androidTestImplementation(libs.hilt.test) androidTestImplementation(libs.work.testing) kspAndroidTest(libs.hilt.test) } fun DependencyHandlerScope.kotlin(name: String): Any = kotlin(name, libs.versions.kotlin.get()) fun versionCodeFor(version: String?): Int? { if (version == null) return null val (major, minor, patch) = version .substringBefore('-') .trim() .split('.') .map { it.toUIntOrNull() } require(major != null && minor != null && patch != null) { "Each segment must be within 0..99 for mapping, was: '$version'" } return (major * 100_000u + minor * 1_000u + patch * 10u).toInt() } val changelogMD by tasks.register("changelogMD") { group = "build" description = "Prepare CHANGELOG.md for release" notCompatibleWithConfigurationCache("Uses Android DSL and Project APIs in task action") doLast { val versionName = requireNotNull(android.defaultConfig.versionName) val changelogMd = rootProject.file("CHANGELOG.md") require(changelogMd.exists()) { "CHANGELOG.md not found at project root" } val lines = changelogMd.readLines() val unreleasedHeaderIdx = lines.indexOfFirst { it.startsWith("## [Unreleased]") } if (unreleasedHeaderIdx == -1) error("No [Unreleased] header found in CHANGELOG.md") if (lines.any { it.startsWith("## [$versionName") }) { logger.warn("Version $versionName already exists in CHANGELOG.md") return@doLast } val today = LocalDate.now(ZoneId.systemDefault()).toString() val newHeader = "## [$versionName] - $today" val updated = lines.toMutableList().apply { add(unreleasedHeaderIdx + 1, "\n" + newHeader) } changelogMd.writeText(updated.joinToString("\n")) } } val fastlaneChangelog by tasks.register("fastlaneChangelog") { group = "build" description = "Prepare Fastlane changelog from CHANGELOG.md and create metadata file" notCompatibleWithConfigurationCache("Uses Android DSL and Project APIs in task action") dependsOn(changelogMD) doLast { val vCode = requireNotNull(android.defaultConfig.versionCode) { "versionCode is not configured" } val vName = requireNotNull(android.defaultConfig.versionName) { "versionName is not configured" } val fastlaneFile = rootProject.file("metadata/en-US/changelogs/${vCode}.txt") if (fastlaneFile.exists()) { logger.warn("Fastlane changelog file already exists: ${fastlaneFile.path}") return@doLast } val changelogs = rootProject.file("CHANGELOG.md").readLines() var blockStartIndex = -1 var blockEndIndex = changelogs.lastIndex for (i in changelogs.indices) { if (changelogs[i].startsWith("## [Unreleased]")) continue if (changelogs[i].startsWith("## [$vName")) { blockStartIndex = i + 1 continue } if (changelogs[i].startsWith("## [") && blockStartIndex != -1) { blockEndIndex = i - 1 break } } val unreleasedBlock = changelogs.subList(blockStartIndex, blockEndIndex + 1) val cleanedForFastlane = unreleasedBlock.joinToString("\n") { raw -> if (raw.startsWith('#')) raw.dropPrefix("### ").trim() else raw.trim() }.trim().ifEmpty { "No changes listed." } fastlaneFile.writeText(cleanedForFastlane) } } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/schemas/com.looker.kenko.data.local.KenkoDatabase/1.json ================================================ { "formatVersion": 1, "database": { "version": 1, "identityHash": "1e8dafafc484e5254c66d1c687eebd72", "entities": [ { "tableName": "Session", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` INTEGER NOT NULL, `sets` TEXT NOT NULL, PRIMARY KEY(`date`))", "fields": [ { "fieldPath": "date", "columnName": "date", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "sets", "columnName": "sets", "affinity": "TEXT", "notNull": true } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "date" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "Exercise", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `target` TEXT NOT NULL, `reference` TEXT, `isIsometric` INTEGER NOT NULL, PRIMARY KEY(`name`))", "fields": [ { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "target", "columnName": "target", "affinity": "TEXT", "notNull": true }, { "fieldPath": "reference", "columnName": "reference", "affinity": "TEXT", "notNull": false }, { "fieldPath": "isIsometric", "columnName": "isIsometric", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "name" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "plan_table", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `exercisesPerDay` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", "fields": [ { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "exercisesPerDay", "columnName": "exercisesPerDay", "affinity": "TEXT", "notNull": true }, { "fieldPath": "isActive", "columnName": "isActive", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": false } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1e8dafafc484e5254c66d1c687eebd72')" ] } } ================================================ FILE: app/schemas/com.looker.kenko.data.local.KenkoDatabase/2.json ================================================ { "formatVersion": 1, "database": { "version": 2, "identityHash": "04f53094bed233a166e553e534b9304f", "entities": [ { "tableName": "sessions", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` INTEGER NOT NULL, `planId` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`planId`) REFERENCES `plans`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", "fields": [ { "fieldPath": "date", "columnName": "date", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "planId", "columnName": "planId", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_sessions_planId", "unique": false, "columnNames": [ "planId" ], "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_sessions_planId` ON `${TABLE_NAME}` (`planId`)" } ], "foreignKeys": [ { "table": "plans", "onDelete": "SET NULL", "onUpdate": "NO ACTION", "columns": [ "planId" ], "referencedColumns": [ "id" ] } ] }, { "tableName": "exercises", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `target` TEXT NOT NULL, `reference` TEXT, `isIsometric` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", "fields": [ { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "target", "columnName": "target", "affinity": "TEXT", "notNull": true }, { "fieldPath": "reference", "columnName": "reference", "affinity": "TEXT", "notNull": false }, { "fieldPath": "isIsometric", "columnName": "isIsometric", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "plans", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `description` TEXT DEFAULT NULL, `difficulty` TEXT DEFAULT NULL, `focus` TEXT DEFAULT NULL, `equipment` TEXT DEFAULT NULL, `time` TEXT DEFAULT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", "fields": [ { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "description", "columnName": "description", "affinity": "TEXT", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "difficulty", "columnName": "difficulty", "affinity": "TEXT", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "focus", "columnName": "focus", "affinity": "TEXT", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "equipment", "columnName": "equipment", "affinity": "TEXT", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "time", "columnName": "time", "affinity": "TEXT", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [], "foreignKeys": [] }, { "tableName": "plan_history", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`planId` INTEGER, `start` INTEGER NOT NULL, `end` INTEGER DEFAULT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`planId`) REFERENCES `plans`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", "fields": [ { "fieldPath": "planId", "columnName": "planId", "affinity": "INTEGER", "notNull": false }, { "fieldPath": "start", "columnName": "start", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "end", "columnName": "end", "affinity": "INTEGER", "notNull": false, "defaultValue": "NULL" }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_plan_history_planId_start_end", "unique": false, "columnNames": [ "planId", "start", "end" ], "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_plan_history_planId_start_end` ON `${TABLE_NAME}` (`planId`, `start`, `end`)" } ], "foreignKeys": [ { "table": "plans", "onDelete": "SET NULL", "onUpdate": "NO ACTION", "columns": [ "planId" ], "referencedColumns": [ "id" ] } ] }, { "tableName": "plan_day", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`planId` INTEGER NOT NULL, `exerciseId` INTEGER NOT NULL, `dayOfWeek` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`planId`) REFERENCES `plans`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`exerciseId`) REFERENCES `exercises`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "planId", "columnName": "planId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "exerciseId", "columnName": "exerciseId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "dayOfWeek", "columnName": "dayOfWeek", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_plan_day_planId_exerciseId", "unique": false, "columnNames": [ "planId", "exerciseId" ], "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_plan_day_planId_exerciseId` ON `${TABLE_NAME}` (`planId`, `exerciseId`)" } ], "foreignKeys": [ { "table": "plans", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "planId" ], "referencedColumns": [ "id" ] }, { "table": "exercises", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "exerciseId" ], "referencedColumns": [ "id" ] } ] }, { "tableName": "sets", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reps` INTEGER NOT NULL, `weight` REAL NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL, `sessionId` INTEGER NOT NULL, `exerciseId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`exerciseId`) REFERENCES `exercises`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sessionId`) REFERENCES `sessions`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "repsOrDuration", "columnName": "reps", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "weight", "columnName": "weight", "affinity": "REAL", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "order", "columnName": "order", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "sessionId", "columnName": "sessionId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "exerciseId", "columnName": "exerciseId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_sets_sessionId_exerciseId", "unique": false, "columnNames": [ "sessionId", "exerciseId" ], "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_sets_sessionId_exerciseId` ON `${TABLE_NAME}` (`sessionId`, `exerciseId`)" } ], "foreignKeys": [ { "table": "exercises", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "exerciseId" ], "referencedColumns": [ "id" ] }, { "table": "sessions", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "sessionId" ], "referencedColumns": [ "id" ] } ] }, { "tableName": "set_type", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `modifier` REAL NOT NULL, PRIMARY KEY(`type`))", "fields": [ { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "modifier", "columnName": "modifier", "affinity": "REAL", "notNull": true } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "type" ] }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '04f53094bed233a166e553e534b9304f')" ] } } ================================================ FILE: app/schemas/com.looker.kenko.data.local.KenkoDatabase/3.json ================================================ { "formatVersion": 1, "database": { "version": 3, "identityHash": "2fb0608c62826bf785ae6108386f6e5b", "entities": [ { "tableName": "sessions", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` INTEGER NOT NULL, `planId` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`planId`) REFERENCES `plans`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", "fields": [ { "fieldPath": "date", "columnName": "date", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "planId", "columnName": "planId", "affinity": "INTEGER" }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_sessions_planId", "unique": false, "columnNames": [ "planId" ], "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_sessions_planId` ON `${TABLE_NAME}` (`planId`)" } ], "foreignKeys": [ { "table": "plans", "onDelete": "SET NULL", "onUpdate": "NO ACTION", "columns": [ "planId" ], "referencedColumns": [ "id" ] } ] }, { "tableName": "exercises", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `target` TEXT NOT NULL, `reference` TEXT, `isIsometric` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", "fields": [ { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "target", "columnName": "target", "affinity": "TEXT", "notNull": true }, { "fieldPath": "reference", "columnName": "reference", "affinity": "TEXT" }, { "fieldPath": "isIsometric", "columnName": "isIsometric", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] } }, { "tableName": "plans", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `description` TEXT DEFAULT NULL, `difficulty` TEXT DEFAULT NULL, `focus` TEXT DEFAULT NULL, `equipment` TEXT DEFAULT NULL, `time` TEXT DEFAULT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", "fields": [ { "fieldPath": "name", "columnName": "name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "description", "columnName": "description", "affinity": "TEXT", "defaultValue": "NULL" }, { "fieldPath": "difficulty", "columnName": "difficulty", "affinity": "TEXT", "defaultValue": "NULL" }, { "fieldPath": "focus", "columnName": "focus", "affinity": "TEXT", "defaultValue": "NULL" }, { "fieldPath": "equipment", "columnName": "equipment", "affinity": "TEXT", "defaultValue": "NULL" }, { "fieldPath": "time", "columnName": "time", "affinity": "TEXT", "defaultValue": "NULL" }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] } }, { "tableName": "plan_history", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`planId` INTEGER, `start` INTEGER NOT NULL, `end` INTEGER DEFAULT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`planId`) REFERENCES `plans`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", "fields": [ { "fieldPath": "planId", "columnName": "planId", "affinity": "INTEGER" }, { "fieldPath": "start", "columnName": "start", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "end", "columnName": "end", "affinity": "INTEGER", "defaultValue": "NULL" }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_plan_history_planId_start_end", "unique": false, "columnNames": [ "planId", "start", "end" ], "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_plan_history_planId_start_end` ON `${TABLE_NAME}` (`planId`, `start`, `end`)" } ], "foreignKeys": [ { "table": "plans", "onDelete": "SET NULL", "onUpdate": "NO ACTION", "columns": [ "planId" ], "referencedColumns": [ "id" ] } ] }, { "tableName": "plan_day", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`planId` INTEGER NOT NULL, `exerciseId` INTEGER NOT NULL, `dayOfWeek` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`planId`) REFERENCES `plans`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`exerciseId`) REFERENCES `exercises`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "planId", "columnName": "planId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "exerciseId", "columnName": "exerciseId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "dayOfWeek", "columnName": "dayOfWeek", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_plan_day_planId_exerciseId", "unique": false, "columnNames": [ "planId", "exerciseId" ], "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_plan_day_planId_exerciseId` ON `${TABLE_NAME}` (`planId`, `exerciseId`)" } ], "foreignKeys": [ { "table": "plans", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "planId" ], "referencedColumns": [ "id" ] }, { "table": "exercises", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "exerciseId" ], "referencedColumns": [ "id" ] } ] }, { "tableName": "sets", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reps` INTEGER NOT NULL, `weight` REAL NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL, `sessionId` INTEGER NOT NULL, `exerciseId` INTEGER NOT NULL, `rir` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`exerciseId`) REFERENCES `exercises`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sessionId`) REFERENCES `sessions`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "repsOrDuration", "columnName": "reps", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "weight", "columnName": "weight", "affinity": "REAL", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "order", "columnName": "order", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "sessionId", "columnName": "sessionId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "exerciseId", "columnName": "exerciseId", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "rir", "columnName": "rir", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [ { "name": "index_sets_sessionId_exerciseId", "unique": false, "columnNames": [ "sessionId", "exerciseId" ], "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_sets_sessionId_exerciseId` ON `${TABLE_NAME}` (`sessionId`, `exerciseId`)" } ], "foreignKeys": [ { "table": "exercises", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "exerciseId" ], "referencedColumns": [ "id" ] }, { "table": "sessions", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "sessionId" ], "referencedColumns": [ "id" ] } ] }, { "tableName": "set_type", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `modifier` REAL NOT NULL, PRIMARY KEY(`type`))", "fields": [ { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "modifier", "columnName": "modifier", "affinity": "REAL", "notNull": true } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "type" ] } } ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2fb0608c62826bf785ae6108386f6e5b')" ] } } ================================================ FILE: app/src/androidTest/kotlin/com/looker/kenko/BackupManagerTest.kt ================================================ /* * 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 . */ package com.looker.kenko import android.content.Context import androidx.core.net.toUri import androidx.test.core.app.ApplicationProvider import com.looker.kenko.data.backup.BackupManager import com.looker.kenko.data.backup.BackupResult import com.looker.kenko.data.local.model.SetType import com.looker.kenko.data.model.PlanItem import com.looker.kenko.data.model.RepsInReserve import com.looker.kenko.data.model.Set import com.looker.kenko.data.model.localDate import com.looker.kenko.data.repository.ExerciseRepo import com.looker.kenko.data.repository.PlanRepo import com.looker.kenko.data.repository.SessionRepo import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import java.io.File import javax.inject.Inject import kotlin.random.Random import kotlin.test.Ignore import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertTrue import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.DayOfWeek import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @HiltAndroidTest class BackupManagerTest { @get:Rule val hiltRule = HiltAndroidRule(this) @Inject lateinit var backupManager: BackupManager @Inject lateinit var sessionRepo: SessionRepo @Inject lateinit var planRepo: PlanRepo @Inject lateinit var exerciseRepo: ExerciseRepo private lateinit var context: Context private lateinit var backupDir: File @Before fun setup() { hiltRule.inject() context = ApplicationProvider.getApplicationContext() backupDir = File(context.cacheDir, "test_backups").apply { mkdirs() } } @After fun tearDown() { backupDir.deleteRecursively() } @Test fun createBackup_createsValidZipFile() = runTest { val backupFile = File(backupDir, "test_backup.zip") val result = backupManager.createBackup(backupFile.toUri()) assertIs(result) assertTrue(backupFile.exists()) assertTrue(backupFile.length() > 0) } @Test fun createBackup_containsDatabaseFile() = runTest { val backupFile = File(backupDir, "test_backup_db.zip") val result = backupManager.createBackup(backupFile.toUri()) assertIs(result) // Verify zip contains database val extractDir = File(backupDir, "extracted") extractDir.mkdirs() java.util.zip.ZipFile(backupFile).use { zip -> val entries = zip.entries().toList().map { it.name } assertTrue(entries.any { it.contains("kenko_database") }) } } @Test @Ignore("Don't know how to restart app so we can check if data was preserved") fun backupAndRestore_preservesData() = runTest { // Create some test data val planId = planRepo.createPlan("backup_test_plan") val exercises = (1..3).mapNotNull { exerciseRepo.get(it) } exercises.forEach { planRepo.addItem( PlanItem( dayOfWeek = DayOfWeek(Random.nextInt(1, 5)), exercise = it, planId = planId, ), ) } planRepo.setCurrent(planId) val sessionId = sessionRepo.getSessionIdOrCreate(localDate) val sets = (1..5).map { Set( repsOrDuration = 12, weight = 50F, type = SetType.Standard, exercise = exercises.first(), rir = RepsInReserve(2), ) } sets.forEach { sessionRepo.addSet(sessionId, it) } // Get counts before backup val exerciseCountBefore = exerciseRepo.stream.first().size val planItemsBefore = planRepo.getPlanItems(planId).size val setCountBefore = sessionRepo.getSets(sessionId).size // Create backup val backupFile = File(backupDir, "data_backup.zip") val backupResult = backupManager.createBackup(backupFile.toUri()) assertIs(backupResult) // Restore backup val restoreResult = backupManager.restoreBackup(backupFile.toUri()) assertIs(restoreResult) // Verify data is preserved val exerciseCountAfter = exerciseRepo.stream.first().size assertEquals(exerciseCountBefore, exerciseCountAfter) } @Test fun restoreBackup_withInvalidFile_returnsError() = runTest { val invalidFile = File(backupDir, "invalid.zip") invalidFile.writeText("not a zip file") val result = backupManager.restoreBackup(invalidFile.toUri()) assertIs(result) } @Test fun restoreBackup_withNonExistentFile_returnsError() = runTest { val nonExistentFile = File(backupDir, "does_not_exist.zip") val result = backupManager.restoreBackup(nonExistentFile.toUri()) assertIs(result) } @Test fun createBackup_withInvalidUri_returnsError() = runTest { val invalidUri = "content://invalid/path".toUri() val result = backupManager.createBackup(invalidUri) assertIs(result) } } ================================================ FILE: app/src/androidTest/kotlin/com/looker/kenko/KenkoTestRunner.kt ================================================ /* * 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 . */ package com.looker.kenko import android.app.Application import android.content.Context import androidx.test.runner.AndroidJUnitRunner import dagger.hilt.android.testing.HiltTestApplication import kotlin.math.pow import kotlin.math.sqrt class KenkoTestRunner : AndroidJUnitRunner() { override fun newApplication( cl: ClassLoader, appName: String, context: Context, ): Application { return super.newApplication( cl, HiltTestApplication::class.java.name, context, ) } } internal inline fun benchmark( repetition: Int, extraMessage: String? = null, block: () -> Long, ): String { if (extraMessage != null) { println("=".repeat(50)) println(extraMessage) println("=".repeat(50)) } val times = DoubleArray(repetition) repeat(repetition) { iteration -> System.gc() System.runFinalization() times[iteration] = block().toDouble() } val meanAndDeviation = times.culledMeanAndDeviation() return buildString { append("=".repeat(50)) append("\n") append(times.joinToString(" | ")) append("\n") append("${meanAndDeviation.first} ms ± ${meanAndDeviation.second.toFloat()} ms") append("\n") append("=".repeat(50)) append("\n") } } private fun DoubleArray.culledMeanAndDeviation(): Pair { sort() return meanAndDeviation() } private fun DoubleArray.meanAndDeviation(): Pair { val mean = average() return mean to sqrt(fold(0.0) { acc, value -> acc + (value - mean).pow(2) } / size) } ================================================ FILE: app/src/androidTest/kotlin/com/looker/kenko/RepositoryTest.kt ================================================ /* * 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 . */ package com.looker.kenko import androidx.test.ext.junit.runners.AndroidJUnit4 import com.looker.kenko.data.local.model.SetType import com.looker.kenko.data.model.PlanItem import com.looker.kenko.data.model.RepsInReserve import com.looker.kenko.data.model.Set import com.looker.kenko.data.model.localDate import com.looker.kenko.data.repository.ExerciseRepo import com.looker.kenko.data.repository.PlanRepo import com.looker.kenko.data.repository.SessionRepo import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject import kotlin.random.Random import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.DayOfWeek import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @HiltAndroidTest @RunWith(AndroidJUnit4::class) class RepositoryTest { @get:Rule val hiltRule = HiltAndroidRule(this) @Inject lateinit var sessionRepo: SessionRepo @Inject lateinit var planRepo: PlanRepo @Inject lateinit var exerciseRepo: ExerciseRepo @Before fun setup() { hiltRule.inject() } @Test fun checkPlanDeletion() = runTest { val planId = planRepo.createPlan("test") val exercises = (1..10).map { exerciseRepo.get(it)!! } exercises.forEach { planRepo.addItem( PlanItem( dayOfWeek = DayOfWeek(Random.nextInt(1, 5)), exercise = it, planId = planId, ), ) } val planItems = planRepo.getPlanItems(planId) assertEquals(10, planItems.size) planRepo.setCurrent(planId) val createdSessionId = sessionRepo.getSessionIdOrCreate(localDate) val stream = sessionRepo.streamByDate(localDate) assertNotNull(stream.first()) val sessionId = stream.first()!!.id!! val sets = (1..24).map { Set( repsOrDuration = 12, weight = 12F, type = SetType.entries.random(), exercise = exercises.random(), rir = RepsInReserve(2) ) } sets.forEach { sessionRepo.addSet(createdSessionId, it) } assertEquals(24, sessionRepo.getSets(sessionId).size) val set = sessionRepo.getSets(sessionId).first() sessionRepo.removeSet(set.id!!) assertEquals(23, sessionRepo.getSets(sessionId).size) val randomPerformedExercise = sets.random().exercise val setsForRandomExercise = sets.filter { it.exercise.id == randomPerformedExercise.id } exerciseRepo.remove(randomPerformedExercise.id!!) assertEquals(23 - setsForRandomExercise.size, sessionRepo.getSets(sessionId).size) planRepo.deletePlan(planId) assertEquals(23 - setsForRandomExercise.size, sessionRepo.getSets(sessionId).size) assertFails { planRepo.addItem( PlanItem( dayOfWeek = DayOfWeek(Random.nextInt(1, 5)), exercise = randomPerformedExercise, planId = planId, ), ) } assertEquals(null, stream.first()!!.planId) val planItemsAfter = planRepo.getPlanItems(planId) assertEquals(0, planItemsAfter.size) } } ================================================ FILE: app/src/androidTest/kotlin/com/looker/kenko/RoomDatabaseTesting.kt ================================================ /* * 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 . */ package com.looker.kenko import androidx.room.Room import androidx.room.testing.MigrationTestHelper import androidx.sqlite.db.SupportSQLiteDatabase import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.looker.kenko.data.local.KenkoDatabase import com.looker.kenko.data.local.MIGRATION_1_2 import com.looker.kenko.data.local.dao.ExerciseDao import com.looker.kenko.data.local.dao.PlanDao import com.looker.kenko.data.local.model.ExerciseEntity import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.utils.EpochDays import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import javax.inject.Inject import kotlin.test.assertContains import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @HiltAndroidTest @RunWith(AndroidJUnit4::class) class RoomDatabaseTesting { private val DB_NAME = "test.db" @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), KenkoDatabase::class.java, ) @get:Rule val hiltRule = HiltAndroidRule(this) @Inject lateinit var exerciseDao: ExerciseDao @Inject lateinit var planDao: PlanDao @Before fun setup() { hiltRule.inject() } @Test fun schemaMigration1To2() = runTest { val db = helper.createDatabase(DB_NAME, 1) db.addV1Data() helper.runMigrationsAndValidate(DB_NAME, 2, true, MIGRATION_1_2) } @Test fun dataMigration1To2() = runTest { val db = helper.createDatabase(DB_NAME, 1) db.addV1Data() val updatedDb = Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().targetContext, KenkoDatabase::class.java, DB_NAME, ).addMigrations(MIGRATION_1_2).build() val exercises = updatedDb.exerciseDao().stream().first() val planHistory = updatedDb.historyDao().getCurrent() assertNotNull(planHistory) assertNotNull(planHistory.planId) val fullHistory = updatedDb.historyDao().getAll() val session = updatedDb.sessionDao().getSession(EpochDays(412)) val emptySession = updatedDb.sessionDao().getSession(EpochDays(413)) val currentPlan = updatedDb.planDao().getPlanById(planHistory.planId) val currentPlanItems = updatedDb.planDao().getPlanItemsByPlanId(planHistory.planId) assertNotNull(session) assertNotNull(emptySession) assertNotNull(currentPlan) assertEquals(session.sets.size, 6) assertContentEquals( session.sets.map { updatedDb.exerciseDao().get(it.exerciseId)?.name ?: "" }, listOf("Pullups", "Rows", "Press", "Shrugs", "Curls", "Plank"), ) assertEquals(emptySession.sets.size, 0) assertEquals(exercises.size, 6) assertTrue(currentPlanItems.all { it.exerciseId != 0 }) assertContains( exercises, ExerciseEntity("Pullups", MuscleGroups.Lats, isIsometric = false, id = 1), ) assertEquals(planHistory.planId, 1) assertEquals(fullHistory.size, 1) println("Current plan id: ${planHistory.planId}") println("Full history: $fullHistory") println("Current Plan: $currentPlan") } @Test fun prepopulatedData() = runTest { val plans = planDao.plansFlow().first() assertTrue(plans.isNotEmpty()) assertTrue(exerciseDao.stream().first().isNotEmpty()) val plan = plans.first() assertTrue(planDao.getPlanItemsByPlanId(plan.id).isNotEmpty()) } private fun SupportSQLiteDatabase.addV1Data() = use { db -> val setJson = arrayOf( "{\"repsOrDuration\":62,\"weight\":44.85,\"type\":\"Standard\",\"exercise\":{\"name\":\"Pullups\",\"target\":\"Lats\",\"reference\":null,\"isIsometric\":false}}", "{\"repsOrDuration\":12,\"weight\":22.84,\"type\":\"Standard\",\"exercise\":{\"name\":\"Rows\",\"target\":\"UpperBack\",\"reference\":null,\"isIsometric\":false}}", "{\"repsOrDuration\":37,\"weight\":68.43,\"type\":\"Standard\",\"exercise\":{\"name\":\"Press\",\"target\":\"Chest\",\"reference\":null,\"isIsometric\":false}}", "{\"repsOrDuration\":90,\"weight\":67.92,\"type\":\"Standard\",\"exercise\":{\"name\":\"Shrugs\",\"target\":\"Traps\",\"reference\":null,\"isIsometric\":false}}", "{\"repsOrDuration\":86,\"weight\":63.94,\"type\":\"Standard\",\"exercise\":{\"name\":\"Curls\",\"target\":\"Biceps\",\"reference\":null,\"isIsometric\":false}}", "{\"repsOrDuration\":86,\"weight\":63.94,\"type\":\"Standard\",\"exercise\":{\"name\":\"Plank\",\"target\":\"Core\",\"reference\":null,\"isIsometric\":true}}", ).joinToString(",") db.execSQL("""INSERT INTO Exercise (name, target, isIsometric) VALUES('Pullups', 'Lats', 0)""") db.execSQL("""INSERT INTO Exercise (name, target, isIsometric) VALUES('Rows', 'UpperBack', 0)""") db.execSQL("""INSERT INTO Exercise (name, target, isIsometric) VALUES('Press', 'Chest', 0)""") db.execSQL("""INSERT INTO Exercise (name, target, isIsometric) VALUES('Shrugs', 'Traps', 0)""") db.execSQL("""INSERT INTO Exercise (name, target, isIsometric) VALUES('Curls', 'Biceps', 0)""") db.execSQL("""INSERT INTO Exercise (name, target, isIsometric) VALUES('Plank', 'Core', 1)""") val plan = "{\"MONDAY\":[{\"name\":\"Pullups\",\"target\":\"Lats\",\"reference\":null,\"isIsometric\":false}],\"TUESDAY\":[{\"name\":\"Rows\",\"target\":\"UpperBack\",\"reference\":null,\"isIsometric\":false}],\"WEDNESDAY\":[{\"name\":\"Press\",\"target\":\"Chest\",\"reference\":null,\"isIsometric\":false}],\"THURSDAY\":[{\"name\":\"Shrugs\",\"target\":\"Traps\",\"reference\":null,\"isIsometric\":false}],\"FRIDAY\":[{\"name\":\"Curls\",\"target\":\"Biceps\",\"reference\":null,\"isIsometric\":false}],\"SUNDAY\":[{\"name\":\"Plank\",\"target\":\"Core\",\"reference\":null,\"isIsometric\":true}]}" val inactivePlan = "{\"MONDAY\":[{\"name\":\"Pullups\",\"target\":\"Lats\",\"reference\":null,\"isIsometric\":false}],\"TUESDAY\":[{\"name\":\"Rows\",\"target\":\"UpperBack\",\"reference\":null,\"isIsometric\":false}],\"WEDNESDAY\":[{\"name\":\"Press\",\"target\":\"Chest\",\"reference\":null,\"isIsometric\":false}],\"THURSDAY\":[{\"name\":\"Shrugs\",\"target\":\"Traps\",\"reference\":null,\"isIsometric\":false}],\"FRIDAY\":[{\"name\":\"Curls\",\"target\":\"Biceps\",\"reference\":null,\"isIsometric\":false}],\"SUNDAY\":[{\"name\":\"Plank\",\"target\":\"Core\",\"reference\":null,\"isIsometric\":true}]}" db.execSQL("""INSERT INTO plan_table (name, exercisesPerDay, isActive) VALUES ('PPL', '$plan', 1)""") db.execSQL("""INSERT INTO plan_table (name, exercisesPerDay, isActive) VALUES ('SAME', '$inactivePlan', 0)""") db.execSQL("""INSERT INTO Session (date, sets) VALUES (412, '[$setJson]')""") db.execSQL("""INSERT INTO Session (date, sets) VALUES (413, '[]')""") } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/kotlin/com/looker/kenko/KenkoApp.kt ================================================ /* * 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 . */ package com.looker.kenko import android.app.Application import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject @HiltAndroidApp class KenkoApp : Application(), Configuration.Provider { @Inject lateinit var workerFactory: HiltWorkerFactory override val workManagerConfiguration: Configuration get() = Configuration.Builder() .setWorkerFactory(workerFactory) .build() } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/KenkoUriHandler.kt ================================================ /* * 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 . */ package com.looker.kenko.data import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import androidx.compose.ui.platform.UriHandler import androidx.core.net.toUri import com.looker.kenko.R class KenkoUriHandler(private val context: Context) : UriHandler { override fun openUri(uri: String) { try { val intent = Intent( Intent.ACTION_VIEW, uri.toUri() ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } catch (_: ActivityNotFoundException) { error(context.getString(R.string.error_invalid_url)) } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/StringHandler.kt ================================================ /* * 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 . */ package com.looker.kenko.data import android.content.Context import android.content.res.Resources class StringHandler(context: Context) { private val resources: Resources = context.resources fun getString(id: Int): String { resources.configuration return resources.getString(id) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/backup/BackupManager.kt ================================================ /* * 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 . */ package com.looker.kenko.data.backup import android.net.Uri import com.looker.kenko.data.model.settings.BackupInterval interface BackupManager { suspend fun createBackup(destinationUri: Uri): BackupResult suspend fun restoreBackup(sourceUri: Uri): BackupResult fun schedulePeriodicBackup(interval: BackupInterval, destinationUri: Uri) fun cancelScheduledBackup() } sealed interface BackupResult { data object Success : BackupResult data class Error(val message: String, val exception: Throwable? = null) : BackupResult } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/backup/BackupManagerImpl.kt ================================================ /* * 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 . */ package com.looker.kenko.data.backup import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf import com.looker.kenko.data.local.KenkoDatabase import com.looker.kenko.data.model.localDate import com.looker.kenko.data.model.settings.BackupInterval import com.looker.kenko.di.IoDispatcher import com.looker.kenko.utils.DateFormat import dagger.hilt.android.qualifiers.ApplicationContext import java.io.File import java.util.concurrent.TimeUnit import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import kotlinx.datetime.LocalDate class BackupManagerImpl @Inject constructor( @param:ApplicationContext private val context: Context, @param:IoDispatcher private val dispatcher: CoroutineDispatcher, private val database: KenkoDatabase, ) : BackupManager { private val databaseName = "kenko_database" private val datastoreFileName = "settings.preferences_pb" override suspend fun createBackup(destinationUri: Uri): BackupResult = withContext(dispatcher) { try { // Checkpoint database to ensure WAL is flushed database.query("PRAGMA wal_checkpoint(TRUNCATE)", null).close() val tempZipFile = File(context.cacheDir, "temp_backup.zip") ZipOutputStream(tempZipFile.outputStream()).use { zipOut -> val dbFiles = databaseFiles() if (dbFiles.isNotEmpty()) dbFiles.forEach { zipOut.addEntry(it.name, it) } val datastoreFile = datastoreFile() if (datastoreFile.exists()) zipOut.addEntry(datastoreFile.name, datastoreFile) } tempZipFile.copyTo(destinationUri) tempZipFile.delete() BackupResult.Success } catch (e: Exception) { e.printStackTrace() BackupResult.Error("Failed to create backup: ${e.message}", e) } } override suspend fun restoreBackup(sourceUri: Uri): BackupResult = withContext(dispatcher) { try { val tempDir = File(context.cacheDir, "temp_restore") tempDir.mkdirs() extractZipFromUri(sourceUri, tempDir) val dbFile = File(tempDir, databaseName) if (!dbFile.exists()) { tempDir.deleteRecursively() return@withContext BackupResult.Error("Invalid backup: database file not found") } database.close() replaceDatabaseFiles(tempDir) replaceDatastoreFile(tempDir) tempDir.deleteRecursively() BackupResult.Success } catch (e: Exception) { BackupResult.Error("Failed to restore backup: ${e.message}", e) } } override fun schedulePeriodicBackup(interval: BackupInterval, destinationUri: Uri) { if (interval == BackupInterval.Off) { cancelScheduledBackup() return } val workRequest = PeriodicWorkRequestBuilder( interval.hours, TimeUnit.HOURS, ).setConstraints( Constraints.Builder() .setRequiresBatteryNotLow(true) .build(), ).setInputData( workDataOf(BackupWorker.KEY_BACKUP_URI to destinationUri.toString()), ).build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( BACKUP_WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, workRequest, ) } override fun cancelScheduledBackup() { WorkManager.getInstance(context).cancelUniqueWork(BACKUP_WORK_NAME) } private fun databaseFiles(): List { val dbPath = context.getDatabasePath(databaseName) return listOf( dbPath, File(dbPath.path + "-shm"), File(dbPath.path + "-wal"), ).filter { it.exists() } } private fun datastoreFile(): File { val datastoreDir = File(context.filesDir, "datastore") return File(datastoreDir, datastoreFileName) } private fun ZipOutputStream.addEntry(name: String, file: File) { putNextEntry(ZipEntry(name)) file.inputStream().use { it.copyTo(this) } closeEntry() } private suspend fun File.copyTo(destinationUri: Uri) = withContext(dispatcher) { if (destinationUri.scheme == "file") { val destFile = File(destinationUri.path!!) copyTo(destFile, overwrite = true) return@withContext } val treeUri = destinationUri.toTreeUri() val treeDoc = DocumentFile.fromTreeUri(context, treeUri) ?: error("Cannot access directory: $treeUri") val fileName = backupFileName(localDate) val backupFile = treeDoc.findFile(fileName) ?: treeDoc.createFile("application/zip", fileName) ?: error("Cannot create backup file in: $treeUri") context.contentResolver.openOutputStream(backupFile.uri)?.use { output -> inputStream().use { input -> input.copyTo(output) } } ?: error("Cannot open output stream for URI: ${backupFile.uri}") } /** * Extracts the tree URI from a potentially malformed URI that may have a filename appended. * Tree URIs have the pattern: content://authority/tree/treeId * If a filename was appended (e.g., /kenko_backup.zip), strip it. */ private fun Uri.toTreeUri(): Uri { val uriString = toString() val treeIndex = uriString.indexOf("/tree/") if (treeIndex == -1) return this val treeStart = treeIndex + "/tree/".length val nextSlash = uriString.indexOf('/', treeStart) return if (nextSlash != -1) { Uri.parse(uriString.take(nextSlash)) } else { this } } private fun extractZipFromUri(sourceUri: Uri, destDir: File) { val inputStream = when (sourceUri.scheme) { "file" -> File(sourceUri.path!!).inputStream() else -> context.contentResolver.openInputStream(sourceUri) ?: error("Cannot open input stream for URI: $sourceUri") } ZipInputStream(inputStream).use { zipIn -> var entry = zipIn.nextEntry while (entry != null) { val file = File(destDir, entry.name) if (!entry.isDirectory) { file.parentFile?.mkdirs() file.outputStream().use { output -> zipIn.copyTo(output) } } zipIn.closeEntry() entry = zipIn.nextEntry } } } private fun replaceDatabaseFiles(sourceDir: File) { val dbPath = context.getDatabasePath(databaseName) val dbFiles = listOf( databaseName, "$databaseName-shm", "$databaseName-wal", ) dbFiles.forEach { fileName -> val sourceFile = File(sourceDir, fileName) val destFile = if (fileName == databaseName) { dbPath } else { File(dbPath.path.replace(databaseName, fileName)) } if (sourceFile.exists()) { destFile.parentFile?.mkdirs() sourceFile.copyTo(destFile, overwrite = true) } else { destFile.delete() } } } private fun replaceDatastoreFile(sourceDir: File) { val sourceFile = File(sourceDir, datastoreFileName) val datastoreDir = File(context.filesDir, "datastore") val destFile = File(datastoreDir, datastoreFileName) if (sourceFile.exists()) { datastoreDir.mkdirs() sourceFile.copyTo(destFile, overwrite = true) } } fun backupFileName(date: LocalDate): String = buildString { append("kenko_backup_") append(DateFormat.BackupName.format(date)) } companion object { const val BACKUP_WORK_NAME = "kenko_backup_work" } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/backup/BackupWorker.kt ================================================ /* * 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 . */ package com.looker.kenko.data.backup import android.content.Context import androidx.core.net.toUri import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import androidx.work.workDataOf import com.looker.kenko.data.repository.SettingsRepo import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlin.time.Clock @HiltWorker class BackupWorker @AssistedInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, private val backupManager: BackupManager, private val settingsRepo: SettingsRepo, ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val backupUri = inputData.getString(KEY_BACKUP_URI)?.toUri() ?: return Result.failure() return when (val result = backupManager.createBackup(backupUri)) { is BackupResult.Success -> { settingsRepo.setLastBackupTime(Clock.System.now()) Result.success(workDataOf(KEY_BACKED_UP_URI to backupUri.toString())) } is BackupResult.Error -> Result.retry() } } companion object { const val KEY_BACKUP_URI = "backup_uri" const val KEY_BACKED_UP_URI = "backup_uri" } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/KenkoDatabase.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import com.looker.kenko.data.local.dao.ExerciseDao import com.looker.kenko.data.local.dao.PerformanceDao import com.looker.kenko.data.local.dao.PlanDao import com.looker.kenko.data.local.dao.PlanHistoryDao import com.looker.kenko.data.local.dao.SessionDao import com.looker.kenko.data.local.dao.SetsDao import com.looker.kenko.data.local.model.ExerciseEntity import com.looker.kenko.data.local.model.PlanDayEntity import com.looker.kenko.data.local.model.PlanEntity import com.looker.kenko.data.local.model.PlanHistoryEntity import com.looker.kenko.data.local.model.SessionDataEntity import com.looker.kenko.data.local.model.SetEntity import com.looker.kenko.data.local.model.SetTypeEntity @Database( version = 3, entities = [ SessionDataEntity::class, ExerciseEntity::class, PlanEntity::class, PlanHistoryEntity::class, PlanDayEntity::class, SetEntity::class, SetTypeEntity::class, ], ) abstract class KenkoDatabase : RoomDatabase() { abstract fun sessionDao(): SessionDao abstract fun exerciseDao(): ExerciseDao abstract fun planDao(): PlanDao abstract fun setsDao(): SetsDao abstract fun historyDao(): PlanHistoryDao abstract fun performanceDao(): PerformanceDao } fun kenkoDatabase(context: Context) = Room .databaseBuilder( context = context, klass = KenkoDatabase::class.java, name = "kenko_database", ) .createFromAsset("kenko.db") .addMigrations( MIGRATION_1_2, MIGRATION_2_3, ) .build() ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/Migrations.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local import android.database.Cursor import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteStatement import com.looker.kenko.data.local.model.ExerciseEntity import com.looker.kenko.data.model.Set import com.looker.kenko.data.model.localDate import kotlinx.datetime.DayOfWeek import kotlinx.datetime.isoDayNumber import kotlinx.datetime.serializers.DayOfWeekSerializer import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.json.Json val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.migrateExercises() db.migratePlans() db.migrateSessions() } private fun SupportSQLiteDatabase.migratePlans() { execSQL( """ CREATE TABLE plans ( `name` TEXT NOT NULL, `description` TEXT DEFAULT NULL, `difficulty` TEXT DEFAULT NULL, `focus` TEXT DEFAULT NULL, `equipment` TEXT DEFAULT NULL, `time` TEXT DEFAULT NULL, `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT) """.trimIndent(), ) execSQL( """ CREATE TABLE plan_day ( `planId` INTEGER NOT NULL CONSTRAINT `fk_sessions_plans_id` REFERENCES `plans` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE, `exerciseId` INTEGER NOT NULL CONSTRAINT `fk_sets_exercises_id` REFERENCES `exercises` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE, `dayOfWeek` INTEGER NOT NULL, `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT) """.trimIndent(), ) createIndex("plan_day", "planId", "exerciseId") execSQL( """ CREATE TABLE plan_history ( `planId` INTEGER CONSTRAINT `fk_sessions_plans_id` REFERENCES `plans` (`id`) ON UPDATE NO ACTION ON DELETE SET NULL, `start` INTEGER NOT NULL, `end` INTEGER DEFAULT NULL, `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT) """.trimIndent(), ) createIndex("plan_history", "planId", "start", "end") execSQL( """ INSERT INTO `plans` (`name`) SELECT `name` FROM `plan_table` """.trimIndent(), ) val planHistorySelectStatement = query("SELECT `id`, `isActive` FROM `plan_table`") val planHistoryInsertStatement = compileStatement( """ INSERT INTO `plan_history` (`planId`, `start`) VALUES (?, ?) """.trimIndent(), ) val selectStatement = query("SELECT `id`, `exercisesPerDay` FROM `plan_table`") val insertStatement = compileStatement( """ INSERT INTO `plan_day` (`dayOfWeek`, `planId`, `exerciseId`) VALUES (?, ?, ?) """.trimIndent(), ) planHistorySelectStatement.toPlanHistory(planHistoryInsertStatement) selectStatement.toPlanDay(this, insertStatement) execSQL("DROP TABLE `plan_table`") } private fun SupportSQLiteDatabase.migrateExercises() { execSQL( """ CREATE TABLE exercises ( `name` TEXT NOT NULL, `target` TEXT NOT NULL, `reference` TEXT, `isIsometric` INTEGER NOT NULL, `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT) """.trimIndent(), ) execSQL( """ INSERT INTO `exercises` (`name`,`target`,`reference`,`isIsometric`) SELECT `name`,`target`,`reference`,`isIsometric` FROM `Exercise` """.trimIndent(), ) execSQL("DROP TABLE `Exercise`") } private fun SupportSQLiteDatabase.migrateSessions() { /** * This `id` can be treated as session id because previous session entity * didn't have any id and creating new id per session is like * creating new session id */ execSQL( """ CREATE TABLE IF NOT EXISTS _tmp_session ( `sets` TEXT NOT NULL, `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT) """.trimIndent(), ) execSQL( """ INSERT INTO `_tmp_session` (`sets`) SELECT (`sets`) FROM `Session` """.trimIndent(), ) execSQL( """ CREATE TABLE IF NOT EXISTS sessions ( `date` INTEGER NOT NULL, `planId` INTEGER CONSTRAINT `fk_sessions_plans_id` REFERENCES `plans` (`id`) ON UPDATE NO ACTION ON DELETE SET NULL, `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT) """.trimIndent(), ) createIndex("sessions", "planId") execSQL( """ INSERT INTO `sessions` (`date`, `planId`) SELECT Session.`date`, plan_history.`planId` FROM `Session` INNER JOIN `plan_history` ON (plan_history.`end` IS NULL AND plan_history.`start` IS NOT NULL) """.trimIndent(), ) execSQL( """ CREATE TABLE IF NOT EXISTS set_type ( `type` TEXT NOT NULL PRIMARY KEY, `modifier` REAL NOT NULL) """.trimIndent() ) execSQL( """ CREATE TABLE IF NOT EXISTS sets ( `reps` INTEGER NOT NULL, `exerciseId` INTEGER NOT NULL CONSTRAINT `fk_sets_exercises_id` REFERENCES `exercises` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE, `weight` REAL NOT NULL, `sessionId` INTEGER NOT NULL CONSTRAINT `fk_sets_sessions_id` REFERENCES `sessions` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE, `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `type` TEXT NOT NULL, `order` INTEGER NOT NULL) """.trimIndent(), ) createIndex("sets", "sessionId", "exerciseId") val selectStatement = query("SELECT `id`, `sets` FROM `_tmp_session`") val insertStatement = compileStatement( """ INSERT INTO `sets` (`reps`, `weight`, `type`, `order`, `sessionId`, `exerciseId`) VALUES (?, ?, ?, ?, ?, ?) """.trimIndent(), ) selectStatement.toSetEntity(this, insertStatement) execSQL("DROP TABLE `Session`") execSQL("DROP TABLE `_tmp_session`") } private fun Cursor.toPlanHistory(insert: SupportSQLiteStatement) { if (moveToFirst()) { val idIndex = getColumnIndex("id") val isActiveIndex = getColumnIndex("isActive") do { val id = getInt(idIndex) val isActive = getInt(isActiveIndex) == 1 if (isActive) insert.insertPlanHistory(id) } while (moveToNext()) } } private fun Cursor.toPlanDay(db: SupportSQLiteDatabase, insert: SupportSQLiteStatement) { if (moveToFirst()) { val idIndex = getColumnIndex("id") val exerciseMapIndex = getColumnIndex("exercisesPerDay") do { val id = getInt(idIndex) val exerciseMapString = getString(exerciseMapIndex) val exerciseMap = Json.decodeFromString(exerciseMapSerializer, exerciseMapString) exerciseMap.forEach { (day, exercises) -> exercises.forEach { exercise -> insert.insertPlanDays(day, id, db.exerciseId(exercise.name)) } } } while (moveToNext()) } } @Suppress("NOTHING_TO_INLINE") private inline fun SupportSQLiteStatement.insertPlanDays( dayOfWeek: DayOfWeek, planId: Int, exerciseId: Int, ) { clearBindings() bindLong(1, dayOfWeek.isoDayNumber.toLong()) bindLong(2, planId.toLong()) bindLong(3, exerciseId.toLong()) executeInsert() } private fun SupportSQLiteDatabase.exerciseId(name: String): Int { val getId = query("SELECT id FROM exercises WHERE name = ?", arrayOf(name)) getId.moveToFirst() return getId.getInt(getId.getColumnIndexOrThrow("id")) } private fun Cursor.toSetEntity(db: SupportSQLiteDatabase, insert: SupportSQLiteStatement) { if (moveToFirst()) { val idIndex = getColumnIndex("id") val setsIndex = getColumnIndex("sets") do { val sessionId = getInt(idIndex) val setsString = getString(setsIndex) val sets = Json.decodeFromString(setsSerializer, setsString) for (i in sets.indices) { insert.insertSet(db, sets[i], sessionId, i) } } while (moveToNext()) } } @Suppress("NOTHING_TO_INLINE") private inline fun SupportSQLiteStatement.insertSet( db: SupportSQLiteDatabase, set: Set, sessionId: Int, order: Int ) { clearBindings() bindLong(1, set.repsOrDuration.toLong()) bindDouble(2, set.weight.toDouble()) bindString(3, set.type.name) bindLong(4, order.toLong()) bindLong(5, sessionId.toLong()) val exerciseId = db.exerciseId(set.exercise.name) bindLong(6, exerciseId.toLong()) executeInsert() } @Suppress("NOTHING_TO_INLINE") private inline fun SupportSQLiteStatement.insertPlanHistory(id: Int) { clearBindings() bindLong(1, id.toLong()) bindLong(2, localDate.toEpochDays().toLong()) executeInsert() } @Suppress("NOTHING_TO_INLINE") private inline fun SupportSQLiteDatabase.createIndex( tableName: String, vararg column: String, ) { val columns = column.joinToString("_") val columnInTable = column.joinToString(",") { "`$it`" } execSQL( """ CREATE INDEX IF NOT EXISTS `index_${tableName}_$columns` ON `$tableName` ($columnInTable) """.trimIndent() ) } private val exerciseMapSerializer = MapSerializer(DayOfWeekSerializer, ListSerializer(ExerciseEntity.serializer())) private val setsSerializer = ListSerializer(Set.serializer()) } val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE sets ADD COLUMN rir INTEGER NOT NULL DEFAULT 2") } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/dao/ExerciseDao.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.dao import androidx.room.Dao import androidx.room.Query import androidx.room.Upsert import com.looker.kenko.data.local.model.ExerciseEntity import kotlinx.coroutines.flow.Flow @Dao interface ExerciseDao { @Upsert suspend fun upsert(exercise: ExerciseEntity) @Query( """ DELETE FROM exercises WHERE id = :id """, ) suspend fun delete(id: Int) @Query( """ SELECT * FROM exercises """, ) fun stream(): Flow> @Query( """ SELECT * FROM exercises WHERE id = :id """, ) suspend fun get(id: Int): ExerciseEntity? @Query( """ SELECT COUNT(*) FROM exercises """, ) fun number(): Flow @Query( """ SELECT EXISTS (SELECT * FROM exercises WHERE name = :name) """, ) suspend fun exists(name: String): Boolean } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/dao/PerformanceDao.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.dao import androidx.room.Dao import androidx.room.RawQuery import androidx.room.Transaction import androidx.room.Upsert import androidx.sqlite.db.SimpleSQLiteQuery import com.looker.kenko.data.local.model.SetTypeEntity import com.looker.kenko.data.repository.Performance @Dao interface PerformanceDao { @Upsert suspend fun upsertSetTypeLookup(type: List) @RawQuery suspend fun _rawQueryRatingWrappers(query: SimpleSQLiteQuery): List? @Transaction suspend fun getPerformance(exerciseId: Int?, planId: Int?): Performance? { val selection = arrayListOf() val query = buildString(1024) { append("SELECT sessions.date, ") // Sum of all ratings append("SUM(") // Ratings = reps * weight * set_type_modifier * rir_modifier append("sets.reps * ") append("sets.weight * ") append("set_type.modifier * ") // RIR modifier append("CASE WHEN sets.rir <= 0 ") append("THEN 1.20 WHEN sets.rir = 1 ") append("THEN 1.12 WHEN sets.rir = 2 ") append("THEN 1.04 WHEN sets.rir = 3 ") append("THEN 0.96 WHEN sets.rir = 4 ") append("THEN 0.88 ELSE 0.80 END") append(") AS rating FROM sets ") append("INNER JOIN set_type ON sets.type = set_type.type ") append("INNER JOIN sessions ON sets.sessionId = sessions.id ") if (exerciseId != null) { append("WHERE (sets.exerciseId = ?) ") selection.add(exerciseId) } if (planId != null) { if (exerciseId != null) { append("AND ") } else { append("WHERE ") } append("sessions.planId = ? ") selection.add(planId) } append("GROUP BY sessions.date ") append("ORDER BY sessions.date ASC") } val ratingWrapper = _rawQueryRatingWrappers( SimpleSQLiteQuery( query = query, bindArgs = selection.toTypedArray(), ), ) return ratingWrapper?.toPerformance() } } class RatingWrapper( val date: Int, val rating: Float, ) fun List.toPerformance(): Performance { val days = IntArray(size) val ratings = FloatArray(size) for (i in indices) { val rating = get(i) days[i] = rating.date ratings[i] = rating.rating } return Performance( days = days, ratings = ratings, ) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/dao/PlanDao.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.dao import androidx.room.Dao import androidx.room.Query import androidx.room.RawQuery import androidx.room.Transaction import androidx.room.Upsert import androidx.sqlite.db.SimpleSQLiteQuery import com.looker.kenko.data.local.model.ExerciseEntity import com.looker.kenko.data.local.model.PlanDayEntity import com.looker.kenko.data.local.model.PlanEntity import com.looker.kenko.data.model.Labels import kotlinx.coroutines.flow.Flow @Dao interface PlanDao { @Query( """ SELECT * FROM plans """, ) fun plansFlow(): Flow> @Transaction @Query( """ SELECT * FROM plan_day WHERE planId = (SELECT planId FROM plan_history WHERE `end` IS NULL AND start IS NOT NULL) ORDER BY id ASC """, ) fun currentPlanItemsFlow(): Flow> @Transaction @Query( """ SELECT * FROM plan_day WHERE planId = (SELECT planId FROM plan_history WHERE `end` IS NULL AND start IS NOT NULL) AND dayOfWeek = :day ORDER BY id ASC """, ) fun currentPlanItemsByDayFlow(day: Int): Flow> @Query( """ SELECT * FROM plans WHERE id = :planId """, ) suspend fun getPlanById(planId: Int): PlanEntity? @Query( """ SELECT EXISTS (SELECT * FROM plans WHERE name = :planName) """, ) suspend fun exists(planName: String): Boolean @Query( """ SELECT * FROM plan_day WHERE planId = :planId ORDER BY id ASC """, ) fun planItemsByPlanIdFlow(planId: Int): Flow> @Query( """ SELECT * FROM plan_day WHERE planId = :planId ORDER BY id ASC """, ) suspend fun getPlanItemsByPlanId(planId: Int): List @Query( """ SELECT * FROM plans WHERE id = (SELECT planId FROM plan_day WHERE exerciseId = :exerciseId) """, ) suspend fun getPlanByExerciseId(exerciseId: Int): List @Transaction @Query( """ SELECT exercises.* FROM exercises INNER JOIN plan_day ON exercises.id = plan_day.exerciseId WHERE plan_day.planId = :planId ORDER BY plan_day.id ASC """, ) fun exerciseByPlanIdFlow(planId: Int): Flow> @Transaction @Query( """ SELECT exercises.* FROM exercises INNER JOIN plan_day ON exercises.id = plan_day.exerciseId WHERE plan_day.planId = :planId ORDER BY plan_day.id ASC """, ) suspend fun getExerciseByPlanId(planId: Int): List @Query( """ SELECT * FROM plan_day WHERE planId = :planId AND dayOfWeek = :day ORDER BY id ASC """, ) fun planItemsByPlanIdAndDayFlow(planId: Int, day: Int): Flow> @Query( """ SELECT * FROM plan_day WHERE planId = :planId AND dayOfWeek = :day ORDER BY id ASC """, ) suspend fun getPlanItemsByPlanIdAndDay(planId: Int, day: Int): List @Query( """ SELECT COUNT(exerciseId) FROM plan_day WHERE planId = :planId """, ) suspend fun getExerciseCountByPlanId(planId: Int): Int @Query( """ SELECT COUNT(DISTINCT dayOfWeek) FROM plan_day WHERE planId = :planId """, ) suspend fun getWorkDaysByPlanId(planId: Int): Int suspend fun searchPlans( query: String? = null, difficulty: Labels.Difficulty? = null, focus: Labels.Focus? = null, equipment: Labels.Equipment? = null, time: Labels.Time? = null, ): List { val args = mutableListOf() val sql = buildString(256) { append("SELECT * FROM plans WHERE 1=1 ") if (!query.isNullOrBlank()) { append("AND name LIKE %?% OR description LIKE %?% ") args.add(query) args.add(query) } if (difficulty != null) { append("AND difficulty = ? ") args.add(difficulty) } if (focus != null) { append("AND focus = ? ") args.add(focus) } if (equipment != null) { append("AND equipment = ? ") args.add(equipment) } if (time != null) { append("AND time = ? ") args.add(time) } } return _rawSearchPlans( SimpleSQLiteQuery( query = sql, bindArgs = args.toTypedArray(), ), ) } @RawQuery suspend fun _rawSearchPlans(query: SimpleSQLiteQuery): List @Upsert suspend fun upsertPlan(plan: PlanEntity): Long @Query("DELETE FROM plans WHERE id = :planId") suspend fun deletePlan(planId: Int) @Query("DELETE FROM plans WHERE id NOT IN (SELECT DISTINCT planId FROM plan_day)") suspend fun deleteEmptyPlans() @Upsert suspend fun insertPlanItem(item: PlanDayEntity) @Query("DELETE FROM plan_day WHERE id = :planDayId") suspend fun deleteItem(planDayId: Long) @Query("DELETE FROM plan_day WHERE exerciseId = :exerciseId") suspend fun deleteItemByExercise(exerciseId: Int) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/dao/PlanHistoryDao.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.dao import androidx.room.Dao import androidx.room.Query import androidx.room.Upsert import com.looker.kenko.data.local.model.PlanHistoryEntity import kotlinx.coroutines.flow.Flow @Dao interface PlanHistoryDao { @Query( """ SELECT planId FROM plan_history WHERE `end` IS NULL AND start IS NOT NULL """, ) fun currentIdFlow(): Flow @Query( """ SELECT planId FROM plan_history WHERE `end` IS NULL AND start IS NOT NULL """, ) suspend fun getCurrentId(): Int? @Query( """ SELECT * FROM plan_history WHERE `end` IS NULL AND start IS NOT NULL """, ) fun currentFlow(): Flow @Query( """ SELECT * FROM plan_history WHERE `end` IS NULL AND start IS NOT NULL """, ) suspend fun getCurrent(): PlanHistoryEntity? @Query( """ SELECT * FROM plan_history """ ) suspend fun getAll(): List @Upsert suspend fun upsert(history: PlanHistoryEntity) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/dao/SessionDao.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import com.looker.kenko.data.local.model.SessionDataEntity import com.looker.kenko.data.local.model.SessionEntity import com.looker.kenko.utils.EpochDays import kotlinx.coroutines.flow.Flow @Dao interface SessionDao { @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insert(session: SessionDataEntity): Long @Query( """ SELECT EXISTS (SELECT * FROM sessions WHERE date = :date) """, ) suspend fun sessionExistsOn(date: EpochDays): Boolean @Query( """ SELECT date FROM sessions WHERE id = :sessionId """, ) suspend fun getDatePerformedOn(sessionId: Int): EpochDays @Query( """ SELECT COUNT(*) FROM sessions """, ) suspend fun getTotalSessions(): Int @Query( """ SELECT id FROM sessions WHERE date = :date """, ) suspend fun getSessionId(date: EpochDays): Int? @Transaction @Query( """ SELECT * FROM sessions ORDER BY date DESC """, ) fun stream(): Flow> @Transaction @Query( """ SELECT * FROM sessions WHERE date = :date """, ) fun session(date: EpochDays): Flow @Transaction @Query( """ SELECT * FROM sessions WHERE date = :date """, ) suspend fun getSession(date: EpochDays): SessionEntity? } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/dao/SetsDao.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.Query import com.looker.kenko.data.local.model.SetEntity import kotlinx.coroutines.flow.Flow @Dao interface SetsDao { @Query( """ SELECT * FROM sets WHERE sessionId = :sessionId ORDER BY `order` """, ) fun setsBySessionId(sessionId: Int): Flow> @Query( """ SELECT * FROM sets WHERE sessionId = :sessionId ORDER BY `order` """, ) suspend fun getSetsBySessionId(sessionId: Int): List @Query( """ SELECT COUNT(*) FROM sets WHERE sessionId = :sessionId """, ) suspend fun getSetsCountBySessionId(sessionId: Int): Int? @Query( """ SELECT * FROM sets WHERE (:exerciseId IS NULL OR exerciseId = :exerciseId) AND sessionId IN ( SELECT id FROM sessions WHERE (:planId IS NULL OR planId = :planId) ) ORDER BY `order` """, ) fun setsByExerciseIdPerPlan(exerciseId: Int? = null, planId: Int? = null): Flow> @Query( """ SELECT * FROM sets WHERE (:exerciseId IS NULL OR exerciseId = :exerciseId) AND sessionId IN ( SELECT id FROM sessions WHERE (:planId IS NULL OR planId = :planId) ) ORDER BY `order` """, ) suspend fun getSetsByExerciseIdPerPlan( exerciseId: Int? = null, planId: Int? = null, ): List @Query( """ SELECT COUNT (*) FROM sets """, ) fun totalSetCount(): Flow @Insert suspend fun insert(set: SetEntity) @Query( """ DELETE FROM sets WHERE id = :setId """, ) suspend fun delete(setId: Int) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/datastore/DatastoreSettingsRepo.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.datastore import androidx.datastore.core.DataStore import androidx.datastore.core.IOException import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import com.looker.kenko.BuildConfig import com.looker.kenko.data.model.settings.BackupInterval import com.looker.kenko.data.model.settings.ColorPalettes import com.looker.kenko.data.model.settings.Settings import com.looker.kenko.data.model.settings.Theme import com.looker.kenko.data.repository.SettingsRepo import javax.inject.Inject import kotlin.time.Instant import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map class DatastoreSettingsRepo @Inject constructor( private val dataStore: DataStore, ) : SettingsRepo { override val stream: Flow get() = dataStore.data .catch { if (it is IOException) error("Error reading datastore") } .map(::mapSettings) override fun get(block: Settings.() -> T): Flow { return stream.map { it.block() } } override suspend fun setOnboardingDone() { if (!BuildConfig.DEBUG) { ONBOARDING_DONE.update(true) } } override suspend fun setColorPalette(colorPalette: ColorPalettes) { COLOR_PALETTE.update(colorPalette.name) } override suspend fun setTheme(theme: Theme) { THEME.update(theme.name) } override suspend fun setLastSetTime(instant: Instant?) { dataStore.edit { preference -> if (instant != null) { preference[LAST_SET_TIME_SECONDS] = instant.epochSeconds } else { preference.remove(LAST_SET_TIME_SECONDS) } } } override suspend fun setBackupUri(uri: String?) { dataStore.edit { preference -> if (uri != null) { preference[BACKUP_URI] = uri } else { preference.remove(BACKUP_URI) } } } override suspend fun setBackupInterval(interval: BackupInterval) { BACKUP_INTERVAL.update(interval.name) } override suspend fun setLastBackupTime(instant: Instant?) { dataStore.edit { preference -> if (instant != null) { preference[LAST_BACKUP_TIME_SECONDS] = instant.epochSeconds } else { preference.remove(LAST_BACKUP_TIME_SECONDS) } } } private suspend inline fun Preferences.Key.update(value: T) { dataStore.edit { preference -> preference[this] = value } } private fun mapSettings(preferences: Preferences): Settings { val isOnboardingDone = preferences[ONBOARDING_DONE] ?: false val theme = preferences[THEME] ?: Theme.System.name val colorPalettes = preferences[COLOR_PALETTE] ?: ColorPalettes.Zestful.name val lastSetTime = preferences[LAST_SET_TIME_SECONDS] val backupUri = preferences[BACKUP_URI] val backupInterval = preferences[BACKUP_INTERVAL] ?: BackupInterval.Off.name val lastBackupTime = preferences[LAST_BACKUP_TIME_SECONDS] return Settings( isOnboardingDone = isOnboardingDone, theme = Theme.valueOf(theme), colorPalette = ColorPalettes.valueOf(colorPalettes), lastSetTime = lastSetTime?.let { Instant.fromEpochSeconds(it) }, backupUri = backupUri, backupInterval = BackupInterval.valueOf(backupInterval), lastBackupTime = lastBackupTime?.let { Instant.fromEpochSeconds(it) }, ) } private companion object Keys { val ONBOARDING_DONE: Preferences.Key = booleanPreferencesKey("onboarding_done") val THEME: Preferences.Key = stringPreferencesKey("theme") val COLOR_PALETTE: Preferences.Key = stringPreferencesKey("color_palette") val LAST_SET_TIME_SECONDS: Preferences.Key = longPreferencesKey("last_set_time_seconds") val BACKUP_URI: Preferences.Key = stringPreferencesKey("backup_uri") val BACKUP_INTERVAL: Preferences.Key = stringPreferencesKey("backup_interval") val LAST_BACKUP_TIME_SECONDS: Preferences.Key = longPreferencesKey("last_backup_time_seconds") } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/model/ExerciseEntity.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.model import androidx.room.Entity import androidx.room.PrimaryKey import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.MuscleGroups import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable @SerialName("exercise") @Entity("exercises") data class ExerciseEntity( val name: String, val target: MuscleGroups, val reference: String? = null, val isIsometric: Boolean = false, @PrimaryKey(autoGenerate = true) val id: Int = 0 ) fun ExerciseEntity.toExternal(): Exercise = Exercise( id = id, name = name, target = target, reference = reference, isIsometric = isIsometric ) fun Exercise.toEntity(): ExerciseEntity = ExerciseEntity( id = id ?: 0, name = name, target = target, reference = reference, isIsometric = isIsometric ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/model/PlanEntity.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.model import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.Labels.Difficulty import com.looker.kenko.data.model.Labels.Equipment import com.looker.kenko.data.model.Labels.Focus import com.looker.kenko.data.model.Labels.Time import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.data.model.Plan import com.looker.kenko.data.model.PlanItem import com.looker.kenko.data.model.PlanStat import kotlinx.datetime.DayOfWeek import kotlinx.datetime.isoDayNumber @Entity(tableName = "plans") data class PlanEntity( val name: String, @ColumnInfo(defaultValue = "NULL") val description: String?, @ColumnInfo(defaultValue = "NULL") val difficulty: Difficulty?, @ColumnInfo(defaultValue = "NULL") val focus: Focus?, @ColumnInfo(defaultValue = "NULL") val equipment: Equipment?, @ColumnInfo(defaultValue = "NULL") val time: Time?, @PrimaryKey(autoGenerate = true) val id: Int = 0, ) @Entity( "plan_day", foreignKeys = [ ForeignKey( entity = PlanEntity::class, parentColumns = ["id"], childColumns = ["planId"], onDelete = ForeignKey.CASCADE, ), ForeignKey( entity = ExerciseEntity::class, parentColumns = ["id"], childColumns = ["exerciseId"], onDelete = ForeignKey.CASCADE, ), ], indices = [ Index("planId", "exerciseId"), ], ) data class PlanDayEntity( val planId: Int, val exerciseId: Int, val dayOfWeek: Int, @PrimaryKey(autoGenerate = true) val id: Long = 0, ) fun PlanEntity.toExternal(isActive: Boolean, stat: PlanStat) = Plan( id = id, name = name, description = description, difficulty = difficulty, focus = focus, equipment = equipment, time = time, stat = stat, isActive = isActive, ) fun Plan.toEntity(): PlanEntity = PlanEntity( id = id ?: 0, name = name, description = description, difficulty = difficulty, focus = focus, equipment = equipment, time = time, ) fun PlanItem.toEntity() = PlanDayEntity( id = id ?: 0, planId = planId, exerciseId = requireNotNull(exercise.id) { "Exercise id cannot be null" }, dayOfWeek = dayOfWeek.isoDayNumber, ) inline fun PlanDayEntity.toExternal(block: (exerciseId: Int) -> Exercise?) = PlanItem( planId = planId, dayOfWeek = DayOfWeek(dayOfWeek), exercise = block(exerciseId) ?: DefaultExercise, id = id, ) val DefaultExercise = Exercise( name = "Exercise Deleted", target = MuscleGroups.Core, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/model/PlanHistoryEntity.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.model import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import com.looker.kenko.utils.EpochDays @Entity( tableName = "plan_history", foreignKeys = [ ForeignKey( entity = PlanEntity::class, parentColumns = ["id"], childColumns = ["planId"], onDelete = ForeignKey.Companion.SET_NULL ) ], indices = [ Index("planId", "start", "end") ] ) data class PlanHistoryEntity( val planId: Int?, val start: EpochDays, @ColumnInfo(defaultValue = "NULL") val end: EpochDays? = null, @PrimaryKey(autoGenerate = true) val id: Long = 0 ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/model/SessionEntity.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.model import androidx.room.ColumnInfo import androidx.room.Embedded import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey import androidx.room.Relation import com.looker.kenko.data.model.Session import com.looker.kenko.data.model.Set import com.looker.kenko.utils.EpochDays import kotlinx.datetime.LocalDate data class SessionEntity( @Embedded val data: SessionDataEntity, @Relation( parentColumn = "id", entityColumn = "sessionId", ) val sets: List, ) @Entity( "sessions", foreignKeys = [ ForeignKey( entity = PlanEntity::class, parentColumns = ["id"], childColumns = ["planId"], onDelete = ForeignKey.SET_NULL, ), ], ) data class SessionDataEntity( val date: EpochDays, @ColumnInfo(index = true) val planId: Int?, @PrimaryKey(autoGenerate = true) val id: Int = 0, ) fun Session.data(): SessionDataEntity = SessionDataEntity( date = EpochDays(date.toEpochDays().toInt()), planId = planId, id = id ?: 0, ) fun Session.sets(): List = sets.map { it.toEntity(id!!, sets.indexOf(it)) } fun SessionEntity.toExternal( setsMap: List, ): Session = Session( planId = data.planId, date = LocalDate.fromEpochDays(data.date.value), sets = setsMap, id = data.id, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/model/SetEntity.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.model import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.RepsInReserve import com.looker.kenko.data.model.Set @Entity( "sets", foreignKeys = [ ForeignKey( entity = ExerciseEntity::class, parentColumns = ["id"], childColumns = ["exerciseId"], onDelete = ForeignKey.CASCADE, ), ForeignKey( entity = SessionDataEntity::class, parentColumns = ["id"], childColumns = ["sessionId"], onDelete = ForeignKey.CASCADE, ), ], indices = [ Index("sessionId", "exerciseId"), ], ) data class SetEntity( @ColumnInfo("reps") val repsOrDuration: Int, val weight: Float, val type: SetType, val order: Int, val sessionId: Int, val exerciseId: Int, val rir: Int = 2, @PrimaryKey(autoGenerate = true) val id: Int = 0, ) fun SetEntity.toExternal(exercise: Exercise): Set = Set( repsOrDuration = repsOrDuration, weight = weight, type = type, exercise = exercise, rir = RepsInReserve(rir), id = id, ) fun Set.toEntity(sessionId: Int, order: Int): SetEntity = SetEntity( id = id ?: 0, repsOrDuration = repsOrDuration, weight = weight, type = type, order = order, sessionId = sessionId, exerciseId = requireNotNull(exercise.id), rir = rir.value, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/local/model/SetTypeEntity.kt ================================================ /* * 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 . */ package com.looker.kenko.data.local.model import androidx.room.Entity import androidx.room.PrimaryKey @Entity("set_type") data class SetTypeEntity( @PrimaryKey val type: SetType, val modifier: Float, ) enum class SetType(val ratingModifier: Float) { Standard(STANDARD_SET_RATING_MODIFIER), Drop(DROP_SET_RATING_MODIFIER), RestPause(REST_PAUSE_SET_RATING_MODIFIER), } private const val STANDARD_SET_RATING_MODIFIER: Float = 1.0F private const val DROP_SET_RATING_MODIFIER: Float = 1.35F private const val REST_PAUSE_SET_RATING_MODIFIER: Float = 1.2F fun defaultSetTypes()= listOf( SetTypeEntity(SetType.Standard, STANDARD_SET_RATING_MODIFIER), SetTypeEntity(SetType.Drop, DROP_SET_RATING_MODIFIER), SetTypeEntity(SetType.RestPause, REST_PAUSE_SET_RATING_MODIFIER), ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/Exercise.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model import androidx.annotation.StringRes import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.looker.kenko.R import com.looker.kenko.data.model.MuscleGroups.Biceps import com.looker.kenko.data.model.MuscleGroups.Calves import com.looker.kenko.data.model.MuscleGroups.Chest import com.looker.kenko.data.model.MuscleGroups.Core import com.looker.kenko.data.model.MuscleGroups.Glutes import com.looker.kenko.data.model.MuscleGroups.Hamstrings import com.looker.kenko.data.model.MuscleGroups.Lats import com.looker.kenko.data.model.MuscleGroups.Quads import com.looker.kenko.data.model.MuscleGroups.Shoulders import com.looker.kenko.data.model.MuscleGroups.Traps import com.looker.kenko.data.model.MuscleGroups.Triceps import com.looker.kenko.data.model.MuscleGroups.UpperBack import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Immutable @Serializable @SerialName("exercise") data class Exercise( val name: String, val target: MuscleGroups, val reference: String? = null, val isIsometric: Boolean = false, val id: Int? = null, ) @Stable val Exercise.repDurationStringRes: Int @StringRes get() = if (isIsometric) R.string.label_duration else R.string.label_reps class ExercisesPreviewParameter : PreviewParameterProvider> { override val values = sequenceOf( listOf( Exercise("Curls", Biceps), Exercise("Barbell Curls", Biceps), Exercise("Preacher Curls", Biceps), Exercise("B-t-B Curls", Biceps), ), listOf( Exercise("Push-down", Triceps), Exercise("Skull-Crushers", Triceps), Exercise("Push-overs", Triceps), ), listOf( Exercise("Lateral Raises", Shoulders), Exercise("Shoulder Press", Shoulders), Exercise("Face Pulls", Shoulders), ), listOf( Exercise("Squats", Quads), Exercise("Leg Press", Quads), Exercise("Hack Squats", Quads), Exercise("Leg Extensions", Quads), ), listOf( Exercise("SDL", Hamstrings), Exercise("Lying Leg Curls", Hamstrings), ), listOf(Exercise("Calve Raises", Calves)), listOf( Exercise("Hip Thrusts", Glutes), Exercise("Lunges", Glutes), ), listOf( Exercise("Sit-ups", Core), Exercise("Leg Raises", Core), ), listOf( Exercise("Bench Press", Chest), Exercise("Incline Bench", Chest), Exercise("Pec Dec", Chest), Exercise("Chest Fly", Chest), ), listOf(Exercise("Shrugs", Traps)), listOf( Exercise("Lat Pull-down", Lats), Exercise("Pull-ups", Lats), Exercise("Lat Prayers", Lats), ), listOf( Exercise("Bent-over Rows", UpperBack), Exercise("Chest-Supported Rows", UpperBack), Exercise("Rows", UpperBack), ), ) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/Labels.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model // TODO: Empahsis on these being a suggestion, "These labels are just suggestions, // even if you are advanced you can use a beginner plan // and change it to your liking and use it" // also clarify how you can recognize your own place in this system, // 1-3 yrs is beginners, and so on sealed interface Labels { enum class Difficulty : Labels { // More free weight exercises, and generally slower plans BEGINNER, INTERMEDIATE, ADVANCED, // Generally need personal additions ADAPTABLE, } enum class Focus : Labels { STRENGTH, HYPERTROPHY, POWER_BUILDING, } enum class Equipment : Labels { FULL_GYM, DUMBBELLS, BARBELLS, NONE, } enum class Time : Labels { QUICK, NORMAL, } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/MuscleGroups.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model import androidx.annotation.StringRes import androidx.compose.runtime.Stable import com.looker.kenko.R import kotlinx.serialization.Serializable @Stable @Serializable enum class MuscleGroups(@StringRes val stringRes: Int) { // Arms Biceps(R.string.label_muscle_biceps), Triceps(R.string.label_muscle_triceps), Shoulders(R.string.label_muscle_shoulders), // Legs Quads(R.string.label_muscle_quads), Hamstrings(R.string.label_muscle_hamstrings), Calves(R.string.label_muscle_calves), Glutes(R.string.label_muscle_glutes), // Front Core(R.string.label_muscle_core), Chest(R.string.label_muscle_chest), // Back Traps(R.string.label_muscle_traps), Lats(R.string.label_muscle_lats), UpperBack(R.string.label_muscle_back), } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/Plan.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model import androidx.compose.runtime.Immutable import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.looker.kenko.data.model.Labels.Difficulty import com.looker.kenko.data.model.Labels.Equipment import com.looker.kenko.data.model.Labels.Focus import com.looker.kenko.data.model.Labels.Time import kotlin.time.Clock import kotlinx.datetime.DatePeriod import kotlinx.datetime.DayOfWeek import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime @Immutable data class Plan( val name: String, val description: String?, val difficulty: Difficulty?, val focus: Focus?, val equipment: Equipment?, val time: Time?, val isActive: Boolean, val stat: PlanStat = PlanStat(0, 0), val id: Int? = null, ) @Immutable data class PlanItem( val dayOfWeek: DayOfWeek, val exercise: Exercise, val planId: Int, val id: Long? = null, ) val localDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date val week = DatePeriod(days = 7) class PlanPreviewParameters : PreviewParameterProvider> { override val values: Sequence> = sequenceOf( listOf( Plan( name = "Push Pull Leg", description = null, difficulty = Difficulty.ADAPTABLE, focus = null, equipment = Equipment.FULL_GYM, time = Time.NORMAL, isActive = true, stat = PlanStat(21, 5), ), Plan( name = "Upper Lower", description = "Alternative upper lower split", difficulty = Difficulty.BEGINNER, focus = Focus.POWER_BUILDING, equipment = Equipment.FULL_GYM, time = Time.QUICK, isActive = false, stat = PlanStat(21, 4), ), Plan( name = "Upper Lower 2", description = "Lower Upper split at home", difficulty = Difficulty.ADAPTABLE, focus = Focus.POWER_BUILDING, equipment = Equipment.DUMBBELLS, time = Time.QUICK, isActive = false, stat = PlanStat(21, 5), ), ), ) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/PlanStat.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.util.packInts import androidx.compose.ui.util.unpackInt1 import androidx.compose.ui.util.unpackInt2 @Immutable @JvmInline value class PlanStat(private val packedInt: Long) { @Stable val exercises: Int get() = unpackInt1(packedInt) @Stable val workDays: Int get() = unpackInt2(packedInt) @Stable val restDays: Int get() = 7 - workDays } fun PlanStat(exercises: Int, workDays: Int): PlanStat { return PlanStat(packInts(exercises, workDays)) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/Rating.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model import androidx.compose.runtime.Immutable @Immutable @JvmInline value class Rating(val value: Float) operator fun Rating.plus(other: Rating) = Rating(value + other.value) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/RepsInReserve.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model import kotlinx.serialization.Serializable @Serializable @JvmInline value class RepsInReserve(val value: Int) { val modifier: Float get() = when { value <= 0 -> 1.20f value == 1 -> 1.12f value == 2 -> 1.04f value == 3 -> 0.96f value == 4 -> 0.88f else -> 0.80f } companion object { fun fromRPE(rpe: Int) = RepsInReserve(10 - rpe) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/Session.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model import androidx.compose.runtime.Immutable import kotlinx.datetime.LocalDate @Immutable data class Session( val date: LocalDate, val sets: List, val planId: Int?, val id: Int? = null, ) { val performExercises: List get() = sets.map { it.exercise }.distinct() } fun Session(planId: Int, sets: List) = Session(planId = planId, date = localDate, sets = sets) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/Set.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model import androidx.compose.runtime.Immutable import com.looker.kenko.data.local.model.SetType import kotlinx.serialization.Serializable @Serializable @Immutable data class Set( val repsOrDuration: Int, val weight: Float, val type: SetType, val exercise: Exercise, val rir: RepsInReserve, val id: Int? = null, ) val Set.rating: Rating get() = Rating(repsOrDuration * weight * type.ratingModifier * rir.modifier) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/settings/BackupInterval.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model.settings import androidx.annotation.StringRes import com.looker.kenko.R enum class BackupInterval( val hours: Long, @param:StringRes val nameRes: Int, ) { Off(0, R.string.label_backup_off), Daily(24, R.string.label_backup_daily), Weekly(168, R.string.label_backup_weekly), Monthly(720, R.string.label_backup_monthly), } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/settings/Settings.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model.settings import kotlin.time.Instant data class Settings( val isOnboardingDone: Boolean, val theme: Theme, val colorPalette: ColorPalettes, val lastSetTime: Instant?, val backupUri: String?, val backupInterval: BackupInterval, val lastBackupTime: Instant?, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/model/settings/Theme.kt ================================================ /* * 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 . */ package com.looker.kenko.data.model.settings import androidx.annotation.StringRes import com.looker.kenko.R import com.looker.kenko.ui.theme.colorSchemes.ColorSchemes import com.looker.kenko.ui.theme.colorSchemes.defaultColorSchemes import com.looker.kenko.ui.theme.colorSchemes.sereneColorSchemes import com.looker.kenko.ui.theme.colorSchemes.twilightColorSchemes import com.looker.kenko.ui.theme.colorSchemes.zestfulColorSchemes enum class Theme(@StringRes val nameRes: Int) { System(R.string.label_theme_system), Light(R.string.label_theme_light), Dark(R.string.label_theme_dark), } enum class ColorPalettes(val scheme: ColorSchemes?) { Dynamic(null), Default(defaultColorSchemes), Zestful(zestfulColorSchemes), Serene(sereneColorSchemes), Twilight(twilightColorSchemes), } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/repository/ExerciseRepo.kt ================================================ /* * 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 . */ package com.looker.kenko.data.repository import com.looker.kenko.data.model.Exercise import kotlinx.coroutines.flow.Flow interface ExerciseRepo { val stream: Flow> val numberOfExercise: Flow suspend fun get(id: Int): Exercise? suspend fun upsert(exercise: Exercise) suspend fun remove(id: Int) suspend fun isExerciseAvailable(name: String): Boolean } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/repository/PerformanceRepo.kt ================================================ /* * 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 . */ package com.looker.kenko.data.repository import androidx.compose.runtime.Immutable interface PerformanceRepo { suspend fun updateModifiers() suspend fun getPerformance(exerciseId: Int? = null, planId: Int? = null): Performance? } @Immutable class Performance( val days: IntArray, val ratings: FloatArray, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/repository/PlanRepo.kt ================================================ /* * 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 . */ package com.looker.kenko.data.repository import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.Labels.Difficulty import com.looker.kenko.data.model.Labels.Equipment import com.looker.kenko.data.model.Labels.Focus import com.looker.kenko.data.model.Labels.Time import com.looker.kenko.data.model.Plan import com.looker.kenko.data.model.PlanItem import kotlinx.coroutines.flow.Flow import kotlinx.datetime.DayOfWeek interface PlanRepo { val plans: Flow> val current: Flow val planItems: Flow> fun planItems(day: DayOfWeek): Flow> fun planItems(id: Int): Flow> fun planItems(id: Int, day: DayOfWeek): Flow> fun activeExercises(day: DayOfWeek): Flow> suspend fun plan(id: Int): Plan? suspend fun planNameExists(name: String): Boolean suspend fun getPlanItems(id: Int): List suspend fun getPlanItems(id: Int, day: DayOfWeek): List suspend fun createPlan( name: String, description: String? = null, difficulty: Difficulty? = null, focus: Focus? = null, equipment: Equipment? = null, time: Time? = null, ): Int suspend fun updatePlan(plan: Plan) suspend fun setCurrent(id: Int) suspend fun deletePlan(id: Int) suspend fun deleteEmptyPlans() suspend fun addItem(planItem: PlanItem) suspend fun removeItem(id: Long) suspend fun removeItemById(exerciseId: Int) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/repository/SessionRepo.kt ================================================ /* * 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 . */ package com.looker.kenko.data.repository import com.looker.kenko.data.local.model.SetType import com.looker.kenko.data.model.RepsInReserve import com.looker.kenko.data.model.Session import com.looker.kenko.data.model.Set import kotlinx.coroutines.flow.Flow import kotlinx.datetime.LocalDate interface SessionRepo { val stream: Flow> val setsCount: Flow suspend fun addSet(sessionId: Int, set: Set) suspend fun addSet( sessionId: Int, exerciseId: Int, weight: Float, reps: Int, setType: SetType, rir: RepsInReserve, ) suspend fun removeSet(setId: Int) suspend fun getSessionIdOrCreate(date: LocalDate): Int fun streamByDate(date: LocalDate): Flow suspend fun getSets(sessionId: Int): List } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/repository/SettingsRepo.kt ================================================ /* * 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 . */ package com.looker.kenko.data.repository import com.looker.kenko.data.model.settings.BackupInterval import com.looker.kenko.data.model.settings.ColorPalettes import com.looker.kenko.data.model.settings.Settings import com.looker.kenko.data.model.settings.Theme import kotlin.time.Instant import kotlinx.coroutines.flow.Flow interface SettingsRepo { val stream: Flow fun get(block: Settings.() -> T): Flow suspend fun setOnboardingDone() suspend fun setColorPalette(colorPalette: ColorPalettes) suspend fun setTheme(theme: Theme) suspend fun setLastSetTime(instant: Instant?) suspend fun setBackupUri(uri: String?) suspend fun setBackupInterval(interval: BackupInterval) suspend fun setLastBackupTime(instant: Instant?) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/repository/local/LocalExerciseRepo.kt ================================================ /* * 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 . */ package com.looker.kenko.data.repository.local import com.looker.kenko.data.local.dao.ExerciseDao import com.looker.kenko.data.local.model.ExerciseEntity import com.looker.kenko.data.local.model.toEntity import com.looker.kenko.data.local.model.toExternal import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.repository.ExerciseRepo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject class LocalExerciseRepo @Inject constructor( private val dao: ExerciseDao, ) : ExerciseRepo { override val stream: Flow> = dao.stream().map { it.map(ExerciseEntity::toExternal) } override val numberOfExercise: Flow = dao.number() override suspend fun get(id: Int): Exercise? = dao.get(id)?.toExternal() override suspend fun upsert(exercise: Exercise) { dao.upsert(exercise.toEntity()) } override suspend fun remove(id: Int) { dao.delete(id) } override suspend fun isExerciseAvailable(name: String): Boolean = dao.exists(name) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/repository/local/LocalPerformanceRepo.kt ================================================ /* * 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 . */ package com.looker.kenko.data.repository.local import com.looker.kenko.data.local.dao.PerformanceDao import com.looker.kenko.data.local.model.defaultSetTypes import com.looker.kenko.data.repository.Performance import com.looker.kenko.data.repository.PerformanceRepo import javax.inject.Inject class LocalPerformanceRepo @Inject constructor( private val performanceDao: PerformanceDao, ) : PerformanceRepo { override suspend fun updateModifiers() { performanceDao.upsertSetTypeLookup(defaultSetTypes()) } override suspend fun getPerformance(exerciseId: Int?, planId: Int?): Performance? = performanceDao.getPerformance(exerciseId, planId) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/repository/local/LocalPlanRepo.kt ================================================ /* * 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 . */ package com.looker.kenko.data.repository.local import com.looker.kenko.data.local.dao.ExerciseDao import com.looker.kenko.data.local.dao.PlanDao import com.looker.kenko.data.local.dao.PlanHistoryDao import com.looker.kenko.data.local.model.PlanEntity import com.looker.kenko.data.local.model.PlanHistoryEntity import com.looker.kenko.data.local.model.toEntity import com.looker.kenko.data.local.model.toExternal import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.Labels import com.looker.kenko.data.model.Plan import com.looker.kenko.data.model.PlanItem import com.looker.kenko.data.model.PlanStat import com.looker.kenko.data.model.localDate import com.looker.kenko.data.repository.PlanRepo import com.looker.kenko.utils.toLocalEpochDays import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.datetime.DayOfWeek import kotlinx.datetime.isoDayNumber class LocalPlanRepo @Inject constructor( private val dao: PlanDao, private val exerciseDao: ExerciseDao, private val historyDao: PlanHistoryDao, ) : PlanRepo { override val plans: Flow> = combine(dao.plansFlow(), historyDao.currentIdFlow()) { plans, current -> plans.map { it.toExternal(isActive = it.id == current, stat = stats(it.id)) } } override val current: Flow = historyDao.currentIdFlow().map { current -> if (current != null) { dao.getPlanById(current)?.toExternal(true, stats(current)) } else { null } } override val planItems: Flow> = dao.currentPlanItemsFlow().map { planDays -> planDays.map { planDay -> planDay.toExternal { exerciseId -> exerciseDao.get(exerciseId)?.toExternal() } } } override fun planItems(day: DayOfWeek): Flow> = dao.currentPlanItemsByDayFlow(day.isoDayNumber).map { planDays -> planDays.map { planDay -> planDay.toExternal { exerciseId -> exerciseDao.get(exerciseId)?.toExternal() } } } override suspend fun plan(id: Int): Plan? { val isCurrent = current.first()?.id return dao.getPlanById(id)?.toExternal(isCurrent == id, stats(id)) } override suspend fun planNameExists(name: String): Boolean = dao.exists(name) override fun planItems(id: Int): Flow> = dao.planItemsByPlanIdFlow(id).map { it.map { planDay -> planDay.toExternal { exerciseId -> exerciseDao.get(exerciseId)?.toExternal() } } } override fun planItems(id: Int, day: DayOfWeek): Flow> = dao.planItemsByPlanIdAndDayFlow(id, day.isoDayNumber).map { it.map { planDay -> planDay.toExternal { exerciseId -> exerciseDao.get(exerciseId)?.toExternal() } } } override fun activeExercises(day: DayOfWeek): Flow> = dao.currentPlanItemsByDayFlow(day.isoDayNumber).map { it.mapNotNull { planDay -> exerciseDao.get(planDay.exerciseId)?.toExternal() } } override suspend fun getPlanItems(id: Int): List = dao.getPlanItemsByPlanId(id).map { it.toExternal { exerciseId -> exerciseDao.get(exerciseId)?.toExternal() } } override suspend fun getPlanItems(id: Int, day: DayOfWeek): List = dao.getPlanItemsByPlanIdAndDay(id, day.isoDayNumber).map { it.toExternal { exerciseId -> exerciseDao.get(exerciseId)?.toExternal() } } private suspend fun stats(id: Int): PlanStat = PlanStat(dao.getExerciseCountByPlanId(id), dao.getWorkDaysByPlanId(id)) override suspend fun createPlan( name: String, description: String?, difficulty: Labels.Difficulty?, focus: Labels.Focus?, equipment: Labels.Equipment?, time: Labels.Time?, ): Int = dao.upsertPlan( PlanEntity( name = name, description = description, difficulty = difficulty, focus = focus, equipment = equipment, time = time, ), ).toInt() override suspend fun updatePlan(plan: Plan) { dao.upsertPlan(plan.toEntity()) } override suspend fun setCurrent(id: Int) { val current = historyDao.getCurrent() if (current != null) { historyDao.upsert(current.copy(end = localDate.toLocalEpochDays())) } historyDao.upsert(PlanHistoryEntity(planId = id, start = localDate.toLocalEpochDays())) } override suspend fun deletePlan(id: Int) { dao.deletePlan(id) } override suspend fun deleteEmptyPlans() { dao.deleteEmptyPlans() } override suspend fun addItem(planItem: PlanItem) { dao.insertPlanItem(planItem.toEntity()) } override suspend fun removeItem(id: Long) { dao.deleteItem(id) } override suspend fun removeItemById(exerciseId: Int) { dao.deleteItemByExercise(exerciseId) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/data/repository/local/LocalSessionRepo.kt ================================================ /* * 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 . */ package com.looker.kenko.data.repository.local import com.looker.kenko.data.local.dao.ExerciseDao import com.looker.kenko.data.local.dao.PlanHistoryDao import com.looker.kenko.data.local.dao.SessionDao import com.looker.kenko.data.local.dao.SetsDao import com.looker.kenko.data.local.model.SessionDataEntity import com.looker.kenko.data.local.model.SetEntity import com.looker.kenko.data.local.model.SetType import com.looker.kenko.data.local.model.toEntity import com.looker.kenko.data.local.model.toExternal import com.looker.kenko.data.model.RepsInReserve import com.looker.kenko.data.model.Session import com.looker.kenko.data.model.Set import com.looker.kenko.data.model.localDate import com.looker.kenko.data.repository.SessionRepo import com.looker.kenko.utils.toLocalEpochDays import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.datetime.LocalDate class LocalSessionRepo @Inject constructor( private val dao: SessionDao, private val setsDao: SetsDao, private val historyDao: PlanHistoryDao, private val exerciseDao: ExerciseDao, ) : SessionRepo { override val stream: Flow> = dao.stream().map { it.map { session -> session.toExternal(session.sets.toExternal()) } } override val setsCount: Flow = setsDao.totalSetCount() override suspend fun addSet(sessionId: Int, set: Set) { setsDao.insert( set.toEntity( sessionId, setsDao.getSetsCountBySessionId(sessionId) ?: 0, ), ) } override suspend fun addSet( sessionId: Int, exerciseId: Int, weight: Float, reps: Int, setType: SetType, rir: RepsInReserve, ) { setsDao.insert( SetEntity( repsOrDuration = reps, weight = weight, exerciseId = exerciseId, sessionId = sessionId, type = setType, order = setsDao.getSetsCountBySessionId(sessionId) ?: 0, rir = rir.value, ), ) } override suspend fun removeSet(setId: Int) { if (!dao.sessionExistsOn(localDate.toLocalEpochDays())) { error("Session does not exist so set cannot be removed") } setsDao.delete(setId) } override suspend fun getSessionIdOrCreate(date: LocalDate): Int { val currentPlanId = requireNotNull(historyDao.getCurrentId()) { "No plan active" } val existingId = dao.getSessionId(date.toLocalEpochDays()) if (existingId != null) { return existingId } return dao.insert(SessionDataEntity(date.toLocalEpochDays(), currentPlanId)).toInt() } override fun streamByDate(date: LocalDate): Flow { return dao .session(date.toLocalEpochDays()) .map { session -> if (session == null) return@map null session.toExternal(session.sets.toExternal()) } } override suspend fun getSets(sessionId: Int): List = setsDao.getSetsBySessionId(sessionId).toExternal() private suspend fun List.toExternal(): List = mapNotNull { val exercise = exerciseDao.get(it.exerciseId) ?: return@mapNotNull null it.toExternal(exercise.toExternal()) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/di/AppModule.kt ================================================ /* * 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 . */ package com.looker.kenko.di import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import javax.inject.Qualifier import javax.inject.Singleton @Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class IoDispatcher @Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class DefaultDispatcher @Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class ApplicationScope @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @IoDispatcher fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO @Provides @DefaultDispatcher fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default @Provides @Singleton @ApplicationScope fun providesCoroutineScope( @DefaultDispatcher dispatcher: CoroutineDispatcher, ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/di/BackupModule.kt ================================================ /* * 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 . */ package com.looker.kenko.di import com.looker.kenko.data.backup.BackupManager import com.looker.kenko.data.backup.BackupManagerImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) abstract class BackupModule { @Binds @Singleton abstract fun bindBackupManager( impl: BackupManagerImpl, ): BackupManager } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/di/DatabaseModule.kt ================================================ /* * 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 . */ package com.looker.kenko.di import android.content.Context import com.looker.kenko.data.local.KenkoDatabase import com.looker.kenko.data.local.dao.ExerciseDao import com.looker.kenko.data.local.dao.PerformanceDao import com.looker.kenko.data.local.dao.PlanDao import com.looker.kenko.data.local.dao.PlanHistoryDao import com.looker.kenko.data.local.dao.SessionDao import com.looker.kenko.data.local.dao.SetsDao import com.looker.kenko.data.local.kenkoDatabase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object DatabaseModule { @Provides @Singleton fun provideDatabase( @ApplicationContext context: Context, ): KenkoDatabase = kenkoDatabase(context) @Provides @Singleton fun provideExerciseDao( database: KenkoDatabase, ): ExerciseDao = database.exerciseDao() @Provides @Singleton fun provideSessionDao( database: KenkoDatabase, ): SessionDao = database.sessionDao() @Provides @Singleton fun providePlanDao( database: KenkoDatabase, ): PlanDao = database.planDao() @Provides @Singleton fun provideSetsDao( database: KenkoDatabase, ): SetsDao = database.setsDao() @Provides @Singleton fun providePlanHistoryDao( database: KenkoDatabase, ): PlanHistoryDao = database.historyDao() @Provides @Singleton fun providePerformanceDao( database: KenkoDatabase, ): PerformanceDao = database.performanceDao() } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/di/DatastoreModule.kt ================================================ /* * 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 . */ package com.looker.kenko.di import android.content.Context import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.preferencesDataStoreFile import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton private const val DATASTORE_FILE_NAME = "settings" @Module @InstallIn(SingletonComponent::class) object DatastoreModule { @Provides @Singleton fun provideDatastore( @ApplicationContext context: Context, ) = PreferenceDataStoreFactory.create( produceFile = { context.preferencesDataStoreFile(DATASTORE_FILE_NAME) } ) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/di/HandlersModule.kt ================================================ /* * 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 . */ package com.looker.kenko.di import android.content.Context import androidx.compose.ui.platform.UriHandler import com.looker.kenko.data.KenkoUriHandler import com.looker.kenko.data.StringHandler import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ViewModelScoped @Module @InstallIn(ViewModelComponent::class) object HandlersModule { @Provides @ViewModelScoped fun provideContext( @ApplicationContext context: Context ): Context = context @Provides @ViewModelScoped fun provideUriHandler( @ApplicationContext context: Context ): UriHandler = KenkoUriHandler(context) @Provides @ViewModelScoped fun provideStringHandler( @ApplicationContext context: Context ): StringHandler = StringHandler(context) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/di/RepositoryModule.kt ================================================ /* * 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 . */ package com.looker.kenko.di import com.looker.kenko.data.local.datastore.DatastoreSettingsRepo import com.looker.kenko.data.repository.ExerciseRepo import com.looker.kenko.data.repository.PerformanceRepo import com.looker.kenko.data.repository.PlanRepo import com.looker.kenko.data.repository.SessionRepo import com.looker.kenko.data.repository.SettingsRepo import com.looker.kenko.data.repository.local.LocalExerciseRepo import com.looker.kenko.data.repository.local.LocalPerformanceRepo import com.looker.kenko.data.repository.local.LocalPlanRepo import com.looker.kenko.data.repository.local.LocalSessionRepo import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) abstract class RepositoryModule { @Binds abstract fun bindSessionRepo( repo: LocalSessionRepo, ): SessionRepo @Binds abstract fun bindPlanRepo( repo: LocalPlanRepo, ): PlanRepo @Binds abstract fun bindExerciseRepo( repo: LocalExerciseRepo, ): ExerciseRepo @Binds abstract fun bindPerformanceRepo( repo: LocalPerformanceRepo, ): PerformanceRepo @Binds abstract fun bindSettingsRepo( repo: DatastoreSettingsRepo, ): SettingsRepo } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/MainActivity.kt ================================================ /* * 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 . */ package com.looker.kenko.ui import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.rememberNavController import com.looker.kenko.ui.getStarted.navigation.GetStartedRoute import com.looker.kenko.ui.navigation.KenkoNavHost import com.looker.kenko.ui.theme.KenkoTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : ComponentActivity() { private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContent { val theme by viewModel.theme.collectAsStateWithLifecycle() val colorScheme by viewModel.colorScheme.collectAsStateWithLifecycle() KenkoTheme( theme = theme, colorSchemes = colorScheme, ) { Kenko { KenkoNavHost( navController = rememberNavController(), startDestination = GetStartedRoute(viewModel.isOnboardingDone), ) } } } } } @Composable fun Kenko( content: @Composable (innerPadding: PaddingValues) -> Unit, ) { Scaffold( modifier = Modifier.fillMaxSize(), containerColor = MaterialTheme.colorScheme.surface, contentWindowInsets = WindowInsets(0), content = content, ) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/MainViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.looker.kenko.data.model.settings.Theme import com.looker.kenko.data.repository.PerformanceRepo import com.looker.kenko.data.repository.SettingsRepo import com.looker.kenko.ui.theme.colorSchemes.ColorSchemes import com.looker.kenko.ui.theme.colorSchemes.zestfulColorSchemes import com.looker.kenko.ui.theme.dynamicColorSchemes import com.looker.kenko.utils.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @HiltViewModel class MainViewModel @Inject constructor( repo: SettingsRepo, performanceRepo: PerformanceRepo, context: Context, ) : ViewModel() { val theme: StateFlow = repo.get { theme } .asStateFlow(Theme.System) val colorScheme: StateFlow = repo.stream .map { it.colorPalette.scheme ?: dynamicColorSchemes(context) ?: zestfulColorSchemes } .asStateFlow(zestfulColorSchemes) val isOnboardingDone: Boolean = runBlocking { repo.stream.first().isOnboardingDone } init { viewModelScope.launch { performanceRepo.updateModifiers() } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/addEditExercise/AddEditExercise.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.addEditExercise import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.looker.kenko.R import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.ui.components.BackButton import com.looker.kenko.ui.components.ErrorSnackbar import com.looker.kenko.ui.components.FlowTargets import com.looker.kenko.ui.components.KenkoButton import com.looker.kenko.ui.components.TargetChip import com.looker.kenko.ui.components.kenkoTextFieldColor import com.looker.kenko.ui.exercises.string import com.looker.kenko.ui.extensions.plus import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme @Composable fun AddEditExercise( onDone: () -> Unit, onBackPress: () -> Unit, ) { val viewModel: AddEditExerciseViewModel = hiltViewModel() val state by viewModel.state.collectAsStateWithLifecycle() AddEditExercise( exerciseName = viewModel.exerciseName, exerciseReference = viewModel.reference, state = state, snackbarState = viewModel.snackbarState, onSelectTarget = viewModel::setTargetMuscle, onSelectIsometric = viewModel::setIsometric, onNameChange = viewModel::setName, onReferenceChange = viewModel::addReference, onBackPress = onBackPress, onDone = { viewModel.addNewExercise(onDone) }, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AddEditExercise( exerciseName: String, exerciseReference: String, state: AddEditExerciseUiState, snackbarState: SnackbarHostState, onSelectTarget: (MuscleGroups) -> Unit, onSelectIsometric: (Boolean) -> Unit, onNameChange: (String) -> Unit, onReferenceChange: (String) -> Unit, onDone: () -> Unit, onBackPress: () -> Unit, ) { Scaffold( topBar = { TopAppBar( navigationIcon = { BackButton(onBackPress) }, title = { Text( text = stringResource( if (exerciseName.isBlank()) { R.string.label_new_exercise } else { R.string.label_edit_exercise } ), ) } ) }, snackbarHost = { SnackbarHost(hostState = snackbarState) { ErrorSnackbar(data = it) } }, ) { innerPadding -> Column( modifier = Modifier .verticalScroll(rememberScrollState()) .padding(PaddingValues(horizontal = 16.dp) + innerPadding), ) { ExerciseTextField( exerciseName = exerciseName, onNameChange = onNameChange, isError = state.isError, isReadOnly = state.isReadOnly, modifier = Modifier.fillMaxWidth() ) Text( modifier = Modifier.padding(vertical = 8.dp), text = stringResource(R.string.label_target), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.outline ) FlowTargets { TargetChip( selected = state.targetMuscle == it, onClick = { onSelectTarget(it) }, text = stringResource(it.string), ) } Spacer(modifier = Modifier.height(12.dp)) IsIsometricButton(isIsometric = state.isIsometric, onChange = onSelectIsometric) Spacer(modifier = Modifier.height(18.dp)) ReferenceTextField( reference = exerciseReference, onReferenceChange = onReferenceChange, isError = state.isReferenceInvalid, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(18.dp)) KenkoButton( modifier = Modifier .align(Alignment.CenterHorizontally) .navigationBarsPadding(), onClick = onDone, label = { Icon( painter = KenkoIcons.Save, contentDescription = null, ) }, icon = { Text(stringResource(R.string.label_save)) } ) Spacer( modifier = Modifier .navigationBarsPadding() .padding(bottom = 8.dp) ) } } } @Composable private fun ReferenceTextField( reference: String, isError: Boolean, onReferenceChange: (String) -> Unit, modifier: Modifier = Modifier, ) { TextField( modifier = modifier, value = reference, onValueChange = onReferenceChange, colors = kenkoTextFieldColor(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), shape = MaterialTheme.shapes.large, supportingText = { Text(text = stringResource(R.string.label_reference_optional)) }, label = { Text(text = stringResource(R.string.label_reference)) }, isError = isError, leadingIcon = { Icon(painter = KenkoIcons.Lightbulb, contentDescription = null) } ) } @Composable private fun ExerciseTextField( exerciseName: String, isError: Boolean, isReadOnly: Boolean, onNameChange: (String) -> Unit, modifier: Modifier = Modifier, ) { TextField( modifier = modifier, value = exerciseName, onValueChange = onNameChange, colors = kenkoTextFieldColor(), readOnly = isReadOnly, shape = MaterialTheme.shapes.large, label = { Text(text = stringResource(R.string.label_name)) }, isError = isError, leadingIcon = { Icon(painter = KenkoIcons.Rename, contentDescription = null) }, supportingText = { if (isError) { Text(text = stringResource(R.string.label_exercise_exists)) } } ) } @Composable private fun IsIsometricButton(isIsometric: Boolean, onChange: (Boolean) -> Unit) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { onChange(!isIsometric) } ) { Column(modifier = Modifier.weight(1F)) { Text( text = stringResource(R.string.label_is_isometric), style = MaterialTheme.typography.bodyLarge, ) Text( text = stringResource(R.string.label_is_isometric_DESC), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline, ) } Switch(checked = isIsometric, onCheckedChange = onChange) } } @PreviewLightDark @Composable private fun ReferenceTextFieldPreview() { KenkoTheme { ReferenceTextField( reference = "https://youtu.be", onReferenceChange = {}, isError = false, modifier = Modifier.fillMaxWidth() ) } } @Preview(showBackground = true) @Composable private fun IsIsometricButtonPreview() { KenkoTheme { var isIso by remember { mutableStateOf(false) } IsIsometricButton(isIsometric = isIso, onChange = { isIso = !isIso }) } } @Preview(name = "Exercise Name Field") @Composable private fun NameTextFieldPreview() { KenkoTheme { ExerciseTextField( exerciseName = "Bench Press", onNameChange = {}, isError = false, isReadOnly = true, modifier = Modifier.fillMaxWidth() ) } } @Preview(name = "Exercise Name Field - Error") @Composable private fun ErrorNameTextFieldPreview() { KenkoTheme { ExerciseTextField( exerciseName = "Bench Press", onNameChange = {}, isError = true, isReadOnly = true, modifier = Modifier.fillMaxWidth() ) } } @Preview(showBackground = true) @Composable private fun AddEditPreview() { KenkoTheme { AddEditExercise( exerciseName = "BenchPress", exerciseReference = "yt.be", state = AddEditExerciseUiState(MuscleGroups.Chest, false, false, false, false), snackbarState = SnackbarHostState(), onSelectTarget = {}, onSelectIsometric = {}, onNameChange = {}, onReferenceChange = {}, onDone = {}, onBackPress = {} ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/addEditExercise/AddEditExerciseViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.addEditExercise import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.looker.kenko.R import com.looker.kenko.data.StringHandler import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.data.repository.ExerciseRepo import com.looker.kenko.ui.addEditExercise.navigation.AddEditExerciseRoute import com.looker.kenko.utils.asStateFlow import com.looker.kenko.utils.isValidUrl import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch @HiltViewModel @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) class AddEditExerciseViewModel @Inject constructor( private val repo: ExerciseRepo, private val stringHandler: StringHandler, savedStateHandle: SavedStateHandle, ) : ViewModel() { private val routeData: AddEditExerciseRoute = savedStateHandle.toRoute() private val exerciseId: Int? = routeData.id private val defaultTarget: MuscleGroups? = routeData.target?.let { MuscleGroups.valueOf(it) } private val targetMuscle = MutableStateFlow(MuscleGroups.Chest) private val isIsometric = MutableStateFlow(false) private val isReadOnly: Boolean = exerciseId != null val snackbarState = SnackbarHostState() var exerciseName: String by mutableStateOf("") private set var reference: String by mutableStateOf("") private set private val isReferenceInvalid = snapshotFlow { reference } .debounce(200.milliseconds) .mapLatest { it.isValidUrl() && it.isNotBlank() } private val exerciseAlreadyExistError = snapshotFlow { exerciseName } .debounce(200.milliseconds) .mapLatest { repo.isExerciseAvailable(it) && !isReadOnly } val state = combine( targetMuscle, isIsometric, flowOf(isReadOnly), exerciseAlreadyExistError, isReferenceInvalid, ) { target, isometric, readOnly, alreadyExist, referenceInvalid -> AddEditExerciseUiState( targetMuscle = target, isIsometric = isometric, isReadOnly = readOnly, isError = alreadyExist, isReferenceInvalid = referenceInvalid, ) }.asStateFlow( AddEditExerciseUiState( targetMuscle = MuscleGroups.Chest, isIsometric = false, isError = false, isReadOnly = false, isReferenceInvalid = false, ), ) fun setName(value: String) { exerciseName = value } fun addReference(value: String) { reference = value } fun setTargetMuscle(value: MuscleGroups) { viewModelScope.launch { targetMuscle.emit(value) } } fun setIsometric(value: Boolean) { viewModelScope.launch { isIsometric.emit(value) } } fun addNewExercise(onDone: () -> Unit) { viewModelScope.launch { if (exerciseName.isBlank()) { snackbarState.showSnackbar(stringHandler.getString(R.string.error_exercise_name_empty)) return@launch } if (state.value.isReferenceInvalid) { snackbarState.showSnackbar(stringHandler.getString(R.string.error_invalid_reference_format)) return@launch } repo.upsert( Exercise( name = exerciseName, target = targetMuscle.value, reference = reference.ifBlank { null }, isIsometric = isIsometric.value, id = exerciseId, ), ) onDone() } } init { viewModelScope.launch { if (exerciseId != null) { val exercise = repo.get(exerciseId) exercise?.let { setName(it.name) addReference(it.reference ?: "") setIsometric(it.isIsometric) setTargetMuscle(it.target) } } else { if (routeData.name != null) setName(routeData.name) if (defaultTarget != null) setTargetMuscle(defaultTarget) } } } } @Stable data class AddEditExerciseUiState( val targetMuscle: MuscleGroups, val isIsometric: Boolean, val isError: Boolean, val isReadOnly: Boolean, val isReferenceInvalid: Boolean, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/addEditExercise/navigation/AddEditExerciseNavigation.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.addEditExercise.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.ui.addEditExercise.AddEditExercise import kotlinx.serialization.Serializable @Serializable data class AddEditExerciseRoute( val id: Int? = null, val name: String? = null, val target: String? = null, ) fun NavController.navigateToAddEditExercise( id: Int? = null, name: String? = null, target: MuscleGroups? = null, navOptions: NavOptions? = null, ) { navigate(AddEditExerciseRoute(id, name, target?.name), navOptions) } fun NavGraphBuilder.addEditExercise( onBackPress: () -> Unit, ) { composable { AddEditExercise(onBackPress, onBackPress) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/addSet/AddSet.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.addSet import androidx.compose.animation.core.Animatable import androidx.compose.foundation.Canvas import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedToggleButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.ToggleButtonDefaults import androidx.compose.material3.toPath import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.center import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Matrix import androidx.compose.ui.graphics.Path import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.graphics.shapes.Morph import androidx.graphics.shapes.RoundedPolygon import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.looker.kenko.R import com.looker.kenko.data.local.model.SetType import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.repDurationStringRes import com.looker.kenko.ui.addSet.AddSetViewModel.FloatTransformation import com.looker.kenko.ui.addSet.AddSetViewModel.IntTransformation import com.looker.kenko.ui.addSet.components.DraggableTextField import com.looker.kenko.ui.addSet.components.rememberDraggableTextFieldState import com.looker.kenko.ui.theme.KenkoIcons import kotlinx.coroutines.launch private val incrementButtonModifier = Modifier .height(48.dp) .zIndex(0f) private val zIndexModifier = Modifier.zIndex(1F) @Composable fun AddSet(exercise: Exercise, onDone: () -> Unit) { val viewModel: AddSetViewModel = hiltViewModel(key = exercise.name) { it.create(exercise.id!!) } Column( modifier = Modifier .padding(horizontal = 16.dp) .wrapContentHeight(), ) { Spacer(modifier = Modifier.height(16.dp)) AddSetHeader( modifier = Modifier.fillMaxWidth(), exerciseName = exercise.name, onClick = { viewModel.addSet() onDone() }, ) Spacer(modifier = Modifier.height(16.dp)) SetTypeSelector( modifier = Modifier.align(CenterHorizontally), selected = viewModel.selectedSetType, onSelect = viewModel::setSetType, ) Spacer(modifier = Modifier.height(16.dp)) SwipeableTextField( modifier = Modifier.align(CenterHorizontally), ) { TextButton( modifier = incrementButtonModifier, onClick = { viewModel.addRep(-1) }, ) { Text(text = stringResource(R.string.label_minus_int, 1)) } val reps = rememberDraggableTextFieldState(viewModel.repsBoundReached) DraggableTextField( dragState = reps, textFieldState = viewModel.reps, inputTransformation = IntTransformation, supportingText = stringResource(exercise.repDurationStringRes), modifier = zIndexModifier, ) TextButton( modifier = incrementButtonModifier, onClick = { viewModel.addRep(1) }, ) { Text(text = stringResource(R.string.label_plus_int, 1)) } TextButton( modifier = incrementButtonModifier, onClick = { viewModel.addRep(5) }, ) { Text(text = stringResource(R.string.label_plus_int, 5)) } } Spacer(modifier = Modifier.height(24.dp)) SwipeableTextField( modifier = Modifier.align(CenterHorizontally), ) { TextButton( modifier = incrementButtonModifier, onClick = { viewModel.addWeight(-1F) }, ) { Text(text = stringResource(R.string.label_minus_int, 1F)) } val weights = rememberDraggableTextFieldState(viewModel.weightsBoundReached) DraggableTextField( dragState = weights, textFieldState = viewModel.weights, supportingText = stringResource(R.string.label_weight), inputTransformation = FloatTransformation, modifier = zIndexModifier, ) TextButton( modifier = incrementButtonModifier, onClick = { viewModel.addWeight(1F) }, ) { Text(text = stringResource(R.string.label_plus_int, 1F)) } TextButton( modifier = incrementButtonModifier, onClick = { viewModel.addWeight(5F) }, ) { Text(text = stringResource(R.string.label_plus_int, 5F)) } } Spacer(modifier = Modifier.height(36.dp)) } } @Composable private fun AddSetHeader( exerciseName: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1F)) { Text( text = stringResource(R.string.label_add_set_for).uppercase(), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.outline, ) Text( text = exerciseName, style = MaterialTheme.typography.displaySmall, color = MaterialTheme.colorScheme.tertiary, ) } FilledTonalIconButton(onClick = onClick) { Icon( painter = KenkoIcons.Done, contentDescription = "", ) } } } @Composable private fun SwipeableTextField( modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit, ) { Surface( modifier = modifier.requiredHeight(48.dp), shape = CircleShape, color = MaterialTheme.colorScheme.surfaceContainerHigh, ) { Row( verticalAlignment = Alignment.CenterVertically, content = content, ) } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun SetTypeSelector( selected: SetType, onSelect: (SetType) -> Unit, modifier: Modifier = Modifier, ) { val options = listOf(SetType.Standard, SetType.Drop, SetType.RestPause) Row( modifier.padding(horizontal = 8.dp), horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), ) { options.forEachIndexed { index, type -> val interactionSource = remember { MutableInteractionSource() } val checked = selected == type OutlinedToggleButton( checked = checked, onCheckedChange = { onSelect(type) }, interactionSource = interactionSource, modifier = Modifier.semantics { role = Role.RadioButton }, border = if (checked) ButtonDefaults.outlinedButtonBorder(true) else null, colors = ToggleButtonDefaults.outlinedToggleButtonColors( checkedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, checkedContentColor = MaterialTheme.colorScheme.onSurface, ), shapes = when (index) { 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() else -> ButtonGroupDefaults.connectedMiddleButtonShapes() }, ) { SetTypeIndicator( selected = checked, type = type, interactionSource = interactionSource, modifier = Modifier.size(12.dp), ) Spacer(Modifier.size(ToggleButtonDefaults.IconSpacing)) Text(text = setTypeLabel(type)) } } } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun SetTypeIndicator( selected: Boolean, type: SetType, interactionSource: MutableInteractionSource, modifier: Modifier = Modifier, ) { val isPressed by interactionSource.collectIsPressedAsState() val morphAnimatable = remember { Animatable(0F) } val morph = remember { Morph(MaterialShapes.Circle, setTypeShape(type)) } val path = remember { Path() } LaunchedEffect(isPressed || selected) { launch { if (isPressed || selected) { morphAnimatable.animateTo(1F) } else { morphAnimatable.animateTo(0F) } } } Canvas(modifier) { drawPath( color = setTypeColor(type), path = processPath( path = morph.toPath(progress = morphAnimatable.value, path = path), size = size, scaleFactor = 1F, ), ) } } private fun processPath( path: Path, size: Size, scaleFactor: Float, scaleMatrix: Matrix = Matrix(), ): Path { scaleMatrix.reset() scaleMatrix.apply { scale(x = size.width * scaleFactor, y = size.height * scaleFactor) } // Scale to the desired size. path.transform(scaleMatrix) // Translate the path to align its center with the available size center. path.translate(size.center - path.getBounds().center) return path } @OptIn(ExperimentalMaterial3ExpressiveApi::class) private fun setTypeShape(type: SetType): RoundedPolygon = when (type) { SetType.Standard -> MaterialShapes.Ghostish SetType.Drop -> MaterialShapes.Arrow SetType.RestPause -> MaterialShapes.Bun } private fun setTypeColor(type: SetType): Color = when (type) { SetType.Standard -> Color(0xFF2196F3) // Blue SetType.Drop -> Color(0xFFFFC107) // Amber/Yellow SetType.RestPause -> Color(0xFFFF7043) // Red/Orange (Deep Orange) } private fun setTypeLabel(type: SetType): String = when (type) { SetType.Standard -> "Standard" SetType.Drop -> "Drop" SetType.RestPause -> "Rest-Pause" } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/addSet/AddSetViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.addSet import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.TextFieldBuffer import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.KeyboardType import androidx.core.text.isDigitsOnly import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.looker.kenko.data.local.model.SetType import com.looker.kenko.data.model.RepsInReserve import com.looker.kenko.data.model.localDate import com.looker.kenko.data.repository.SessionRepo import com.looker.kenko.ui.addSet.components.BoundReached import com.looker.kenko.ui.addSet.components.Direction import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @HiltViewModel(assistedFactory = AddSetViewModel.AddSetViewModelFactory::class) class AddSetViewModel @AssistedInject constructor( private val sessionRepo: SessionRepo, @Assisted private val id: Int, ) : ViewModel() { val reps: TextFieldState = TextFieldState("12") val weights: TextFieldState = TextFieldState("20.0") var selectedSetType by mutableStateOf(SetType.Standard) private set fun setSetType(type: SetType) { selectedSetType = type } fun addRep(value: Int) { reps.setTextAndPlaceCursorAtEnd((repInt + value).toString()) } fun addWeight(value: Float) { weights.setTextAndPlaceCursorAtEnd((weightFloat + value).toString()) } val repsBoundReached = BoundReached { direction -> when (direction) { Direction.Left -> addRep(-1) Direction.Right -> addRep(1) } } val weightsBoundReached = BoundReached { direction -> when (direction) { Direction.Left -> addWeight(-1F) Direction.Right -> addWeight(1F) } } fun addSet() { viewModelScope.launch { val sessionId = sessionRepo.getSessionIdOrCreate(localDate) sessionRepo.addSet( sessionId = sessionId, exerciseId = id, weight = weightFloat, reps = repInt, setType = selectedSetType, rir = RepsInReserve(2), ) } } private inline val repInt: Int get() = reps.text.toString().toIntOrNull() ?: 0 private inline val weightFloat: Float get() = weights.text.toString().toFloatOrNull() ?: 0F @AssistedFactory interface AddSetViewModelFactory { fun create(id: Int): AddSetViewModel } object IntTransformation : InputTransformation { override val keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) override fun TextFieldBuffer.transformInput() { if (!asCharSequence().isDigitsOnly()) { revertAllChanges() } } } object FloatTransformation : InputTransformation { override val keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) override fun TextFieldBuffer.transformInput() { toString().toFloatOrNull() ?: revertAllChanges() } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/addSet/components/DragState.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.addSet.components import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.DraggableState import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import com.looker.kenko.ui.components.DpRange import com.looker.kenko.ui.components.contains import com.looker.kenko.ui.components.rangeTo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch private const val OFFSET_DAMPING = 0.998F @Stable class DragState( density: Density, swipeRange: DpRange, noIncrementRange: DpRange, incrementDelay: Long, private val events: BoundReached, private val scope: CoroutineScope, ) { val offset: Animatable = Animatable(0F) private val highBouncySpring = spring(Spring.DampingRatioHighBouncy) private val mediumBouncySpring = spring(Spring.DampingRatioMediumBouncy) val state: DraggableState = DraggableState { delta -> val targetOffset = offset.value + delta val adjustedOffset = OFFSET_DAMPING * targetOffset scope.launch { offset.animateTo(adjustedOffset, highBouncySpring) } } @Stable fun onDragStop(velocity: Float) { scope.launch { offset.animateTo( targetValue = 0F, animationSpec = mediumBouncySpring, initialVelocity = velocity, ) } } init { with(density) { offset.updateBounds( swipeRange.start.toPx(), swipeRange.end.toPx(), ) scope.launch { while (true) { val isOutsideBounds = offset.value !in noIncrementRange if (isOutsideBounds) { val direction = if (offset.value > 0) Direction.Right else Direction.Left events.onReached(direction) } delay(incrementDelay) } } } } } @Composable fun rememberDraggableTextFieldState( onBoundReached: BoundReached, incrementDelay: Long = 200, swipeRange: DpRange = (-48).dp..96.dp, noIncrementRange: DpRange = (-24).dp..24.dp, ): DragState { val density = LocalDensity.current val scope = rememberCoroutineScope() return remember { DragState( density = density, swipeRange = swipeRange, noIncrementRange = noIncrementRange, events = onBoundReached, scope = scope, incrementDelay = incrementDelay, ) } } @Stable fun interface BoundReached { fun onReached(direction: Direction) } @JvmInline @Immutable value class Direction private constructor(val value: Int) { companion object { val Left = Direction(1) val Right = Direction(2) } override fun toString(): String { return when (this) { Left -> "Left" Right -> "Right" else -> "Invalid" } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/addSet/components/DraggableTextField.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.addSet.components import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.requiredWidthIn import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.looker.kenko.ui.components.kenkoTextDecorator @Composable fun DraggableTextField( dragState: DragState, textFieldState: TextFieldState, supportingText: String, inputTransformation: InputTransformation, modifier: Modifier = Modifier, containerColor: Color = MaterialTheme.colorScheme.secondaryContainer, textColor: Color = MaterialTheme.colorScheme.onSecondaryContainer, style: TextStyle = MaterialTheme.typography.titleMedium.copy(color = textColor), options: KeyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, imeAction = ImeAction.Next, ), ) { BasicTextField( modifier = modifier .requiredHeight(40.dp) .requiredWidthIn(40.dp) .wrapContentWidth() .graphicsLayer { translationX = dragState.offset.value } .draggable( orientation = Orientation.Horizontal, state = dragState.state, startDragImmediately = true, onDragStopped = { dragState.onDragStop(it) }, ) .clip(CircleShape) .background(containerColor), state = textFieldState, lineLimits = TextFieldLineLimits.SingleLine, inputTransformation = inputTransformation, cursorBrush = SolidColor(MaterialTheme.colorScheme.onSecondaryContainer), keyboardOptions = options, decorator = kenkoTextDecorator(supportingText), textStyle = style.copy(textAlign = TextAlign.Center), ) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/Border.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.foundation.BorderStroke import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp val KenkoBorderWidth: Dp = 1.4.dp val PrimaryBorder: BorderStroke @Composable get() = BorderStroke(KenkoBorderWidth, MaterialTheme.colorScheme.primary) val SecondaryBorder: BorderStroke @Composable get() = BorderStroke(KenkoBorderWidth, MaterialTheme.colorScheme.secondary) val OutlineBorder: BorderStroke @Composable get() = BorderStroke(KenkoBorderWidth, MaterialTheme.colorScheme.secondary) val OnSurfaceBorder: BorderStroke @Composable get() = BorderStroke(KenkoBorderWidth, MaterialTheme.colorScheme.onSurface) val OnSurfaceVariantBorder: BorderStroke @Composable get() = BorderStroke(KenkoBorderWidth, MaterialTheme.colorScheme.onSurfaceVariant) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/Button.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonColors import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.looker.kenko.ui.theme.KenkoIcons @Composable fun KenkoButton( modifier: Modifier = Modifier, onClick: () -> Unit, label: @Composable () -> Unit, icon: @Composable () -> Unit, ) { Button( modifier = modifier, onClick = onClick, contentPadding = PaddingValues( vertical = 24.dp, horizontal = 40.dp ), ) { label() Spacer(modifier = Modifier.width(8.dp)) icon() } } @Composable fun SecondaryKenkoButton( modifier: Modifier = Modifier, onClick: () -> Unit, label: @Composable () -> Unit, icon: @Composable () -> Unit, ) { Button( modifier = modifier, onClick = onClick, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer, ), contentPadding = PaddingValues( vertical = 24.dp, horizontal = 40.dp ), ) { label() Spacer(modifier = Modifier.width(8.dp)) icon() } } @Composable fun TertiaryKenkoButton( modifier: Modifier = Modifier, onClick: () -> Unit, label: @Composable () -> Unit, icon: @Composable () -> Unit, ) { Button( modifier = modifier, onClick = onClick, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.tertiary, contentColor = MaterialTheme.colorScheme.onTertiary, ), contentPadding = PaddingValues( vertical = 24.dp, horizontal = 40.dp ), ) { label() Spacer(modifier = Modifier.width(8.dp)) Box(Modifier.size(18.dp)) { icon() } } } @Composable fun BackButton( onClick: () -> Unit, modifier: Modifier = Modifier, colors: IconButtonColors = IconButtonDefaults.iconButtonColors(), ) { IconButton( modifier = modifier, colors = colors, onClick = onClick, ) { Icon(painter = KenkoIcons.ArrowBack, contentDescription = null) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/Days.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.animation.animateColor import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import kotlinx.datetime.DayOfWeek @Composable fun HorizontalDaySelector( item: @Composable (DayOfWeek) -> Unit, modifier: Modifier = Modifier, ) { LazyRow( modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, ) { items(DayOfWeek.entries) { item(it) } } } @Composable fun DaySelectorChip( selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { val transition = updateTransition(selected, label = "Day Selector") val background by transition.animateColor(label = "Color") { if (it) { MaterialTheme.colorScheme.secondaryContainer } else { MaterialTheme.colorScheme.surfaceContainer } } val corner by transition.animateDp(label = "Corner") { if (it) { 28.dp } else { 12.dp } } Box( modifier = Modifier .padding(horizontal = 4.dp) .graphicsLayer { clip = true shape = RoundedCornerShape(corner) } .drawBehind { drawRect(color = background) } .clickable(onClick = onClick) .padding(vertical = 8.dp, horizontal = 12.dp) .then(modifier), contentAlignment = Alignment.Center, ) { CompositionLocalProvider( LocalTextStyle provides MaterialTheme.typography.labelLarge, LocalContentColor provides MaterialTheme.colorScheme.onSurface, content = content, ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/Dp.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.util.packFloats import androidx.compose.ui.util.unpackFloat1 import androidx.compose.ui.util.unpackFloat2 @Immutable @JvmInline value class DpRange(private val packedFloat: Long) { @Stable val start: Dp get() = Dp(unpackFloat1(packedFloat)) @Stable val end: Dp get() = Dp(unpackFloat2(packedFloat)) } operator fun Dp.rangeTo(other: Dp): DpRange = DpRange(packFloats(value, other.value)) context(density: Density) operator fun DpRange.contains(other: Float): Boolean = with(density) { other > start.toPx() && other < end.toPx() } operator fun DpRange.contains(other: Dp): Boolean = other > start && other < end ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/EmptyPageIndicator.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun EmptyPage( text: String, modifier: Modifier = Modifier, hero: @Composable () -> Unit = { ContainedLoadingIndicator() }, ) { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.fillMaxSize(), ) { hero() Spacer(Modifier.height(12.dp)) Text( text = text, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.outline, ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/Labels.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import com.looker.kenko.R @Composable fun LiftingQuotes( modifier: Modifier = Modifier, color: Color = MaterialTheme.colorScheme.onSurfaceVariant, style: TextStyle = MaterialTheme.typography.labelSmall, ) { val array = stringArrayResource(R.array.label_lifting_quotes) val randomQuote = remember { array.random() } Text( text = randomQuote, style = style, color = color, modifier = Modifier .padding(top = 8.dp, bottom = 6.dp) .then(modifier), ) } @Composable fun HealthQuotes( modifier: Modifier = Modifier, color: Color = MaterialTheme.colorScheme.onSurfaceVariant, style: TextStyle = MaterialTheme.typography.labelSmall, ) { val array = stringArrayResource(R.array.label_health_quotes) val randomQuote = remember { array.random() } Text( text = randomQuote, style = style, color = color, modifier = Modifier .padding(top = 8.dp, bottom = 6.dp) .then(modifier), ) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/List.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp private val FAB_PADDING = 88.dp fun LazyListScope.endItem( height: Dp = FAB_PADDING, ) { item { Spacer(Modifier.height(height)) // TODO: Add some item here to make list end look good behind FAB } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/ReferenceItem.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.looker.kenko.R import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme @Composable fun ReferenceItem( onClick: () -> Unit, modifier: Modifier = Modifier, ) { Surface( modifier = modifier, shape = MaterialTheme.shapes.extraLarge, color = MaterialTheme.colorScheme.surfaceContainer, onClick = onClick ) { Row( modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { Icon(painter = KenkoIcons.Lightbulb, contentDescription = null) Spacer(modifier = Modifier.width(8.dp)) Text( text = stringResource(R.string.label_reference), style = MaterialTheme.typography.titleMedium ) Spacer(modifier = Modifier.weight(1F)) FilledTonalIconButton(onClick = onClick) { Icon(painter = KenkoIcons.ArrowOutward, contentDescription = null) } } } } @Preview @Composable private fun ReferenceItemPreview() { KenkoTheme { ReferenceItem(onClick = {}) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/Snackbar.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarData import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarVisuals import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import com.looker.kenko.ui.theme.KenkoTheme @Composable fun ErrorSnackbar( data: SnackbarData, modifier: Modifier = Modifier, ) { Snackbar( modifier = modifier.heightIn(84.dp), shape = CircleShape, containerColor = MaterialTheme.colorScheme.error, contentColor = MaterialTheme.colorScheme.onError, dismissActionContentColor = MaterialTheme.colorScheme.onError, snackbarData = data, ) } @PreviewLightDark @Composable private fun SnackbarPreview() { KenkoTheme { ErrorSnackbar( data = object : SnackbarData { override val visuals: SnackbarVisuals get() = object : SnackbarVisuals { override val actionLabel: String? get() = null override val duration: SnackbarDuration get() = SnackbarDuration.Long override val message: String get() = "Error" override val withDismissAction: Boolean get() = false } override fun dismiss() {} override fun performAction() {} } ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/SwipeToDeleteBox.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.AnchoredDraggableDefaults import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.gestures.snapTo import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.looker.kenko.ui.theme.KenkoIcons import kotlinx.coroutines.launch import kotlin.math.roundToInt @Composable fun SwipeToDeleteBox( onDismiss: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { val density = LocalDensity.current val scope = rememberCoroutineScope() val actionWidth = 96.dp val actionWidthPx = with(density) { actionWidth.toPx() } val anchors = remember { DraggableAnchors { DragPositions.Settle at 0F DragPositions.End at actionWidthPx } } val state = remember { AnchoredDraggableState( initialValue = DragPositions.Settle, anchors = anchors, ) } val isOutsideBound by remember { derivedStateOf { state.currentValue == DragPositions.End } } val background by animateColorAsState( targetValue = if (isOutsideBound) { MaterialTheme.colorScheme.errorContainer } else { MaterialTheme.colorScheme.surfaceContainer }, label = "", ) Box( modifier = modifier .height(IntrinsicSize.Min) .drawBehind { drawRect(background) }, ) { Box( modifier = Modifier .matchParentSize() .align(Alignment.CenterEnd) .clickable { scope.launch { state.snapTo(DragPositions.Settle) onDismiss() } }, contentAlignment = Alignment.Center, ) { Icon( modifier = Modifier .requiredWidth(actionWidth) .align(Alignment.CenterEnd), painter = KenkoIcons.Delete, tint = MaterialTheme.colorScheme.onErrorContainer, contentDescription = null, ) } Box( modifier = Modifier .fillMaxWidth() .offset { IntOffset( x = -state .requireOffset() .roundToInt(), y = 0, ) } .graphicsLayer { val offset = state.requireOffset() val scale = 1 - (offset / actionWidthPx) val cornerRadius = actionWidthPx / 4F clip = true scaleX = scale.coerceAtLeast(0.9F) scaleY = scale.coerceAtLeast(0.9F) shape = RoundedCornerShape(cornerRadius * ((offset / actionWidthPx))) } .anchoredDraggable( state = state, orientation = Orientation.Horizontal, reverseDirection = true, flingBehavior = AnchoredDraggableDefaults.flingBehavior( state = state, positionalThreshold = { distance: Float -> distance * 0.5F }, animationSpec = tween(), ), ), ) { content() } } } @Composable fun disableScrollConnection() = remember { object : NestedScrollConnection { override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource, ): Offset { return available } } } private enum class DragPositions { Settle, End, } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/Targets.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.FilterChip import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.ui.exercises.string import com.looker.kenko.ui.theme.KenkoTheme private val SortedTargets = MuscleGroups.entries.sortedBy { it.string } private val Targets = listOf(null) + SortedTargets @Composable fun FlowTargets( modifier: Modifier = Modifier, content: @Composable (MuscleGroups) -> Unit, ) { FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier, ) { SortedTargets.forEach { target -> content(target) } } } @Composable fun LazyTargets( modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), content: @Composable (MuscleGroups?) -> Unit, ) { LazyRow( horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = contentPadding, modifier = modifier, ) { items(Targets) { target -> content(target) } } } @Composable fun TargetChip( selected: Boolean, onClick: () -> Unit, text: String, modifier: Modifier = Modifier, ) { FilterChip( selected = selected, onClick = onClick, label = { Text(text = text) }, modifier = modifier, ) } @Composable fun HorizontalTargetChips( target: MuscleGroups?, onSelect: (MuscleGroups?) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), ) { Row( modifier = modifier .fillMaxWidth() .horizontalScroll(rememberScrollState()) .padding(bottom = 4.dp), ) { Spacer(modifier = Modifier.width(contentPadding.calculateStartPadding(LocalLayoutDirection.current))) val sortedTargets = remember { Targets.sortedBy { it?.string } } sortedTargets.forEachIndexed { index, muscle -> FilterChip( selected = target == muscle, onClick = { onSelect(muscle) }, label = { Text(text = stringResource(muscle.string)) }, ) if (sortedTargets.lastIndex != index) { Spacer(modifier = Modifier.width(8.dp)) } } Spacer(modifier = Modifier.width(contentPadding.calculateEndPadding(LocalLayoutDirection.current))) } } @Preview(showBackground = true) @Composable private fun HorizontalTargetChipsPreview() { KenkoTheme { LazyTargets { TargetChip( selected = it == null, onClick = {}, text = stringResource(it.string), modifier = Modifier.padding(horizontal = 4.dp), ) } } } @Preview(showBackground = true) @Composable private fun FlowTargetChipsPreview() { KenkoTheme { FlowTargets { TargetChip( selected = false, onClick = {}, text = stringResource(it.stringRes), modifier = Modifier.padding(horizontal = 4.dp), ) } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/Text.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @Composable fun TypingText( text: String, modifier: Modifier = Modifier, startTyping: Boolean = true, style: TextStyle = LocalTextStyle.current, color: Color = LocalContentColor.current, initialDelay: Duration = 100.milliseconds, typingDelay: Duration = 50.milliseconds, onCompleteListener: (() -> Unit)? = null, ) { var substringText by remember { mutableStateOf("") } val textArray = remember(text) { CharArray(text.length) } LaunchedEffect(text, startTyping) { if (startTyping) { delay(initialDelay) for (i in textArray.indices) { textArray[i] = text[i] substringText = textArray.concatToString() delay(typingDelay) } onCompleteListener?.invoke() } } Text( modifier = modifier, text = substringText, style = style, color = color, ) } @Composable fun TickerText( text: String, modifier: Modifier = Modifier, style: TextStyle = MaterialTheme.typography.labelLarge, color: Color = LocalContentColor.current, ) { val tickerMarquee = remember { List(10) { text }.joinToString( separator = " ${Typography.bullet} ", prefix = " ${Typography.bullet} ", ) } Column( modifier = modifier, verticalArrangement = Arrangement.Center, ) { Text( modifier = Modifier .padding(vertical = 4.dp) .basicMarquee( initialDelayMillis = 0, iterations = Int.MAX_VALUE, ), text = tickerMarquee, style = style, color = color, ) } } @Composable fun TickerText( texts: Array, modifier: Modifier = Modifier, style: TextStyle = MaterialTheme.typography.labelLarge, color: Color = LocalContentColor.current, ) { val tickerMarquee = remember { texts.joinToString( separator = " ${Typography.bullet} ", prefix = " ${Typography.bullet} ", ) } Column( modifier = modifier, verticalArrangement = Arrangement.Center, ) { Text( modifier = Modifier .padding(vertical = 4.dp) .basicMarquee( initialDelayMillis = 0, iterations = Int.MAX_VALUE, ), text = tickerMarquee, style = style, color = color, ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/TextField.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.input.TextFieldDecorator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp fun kenkoTextDecorator(supportingText: String) = TextFieldDecorator { val outlineColor = MaterialTheme.colorScheme.outline Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( text = supportingText, style = MaterialTheme.typography.labelSmall, color = outlineColor, ) it() HorizontalDivider( modifier = Modifier.width(48.dp), color = outlineColor, ) } } @Composable fun kenkoTextFieldColor(): TextFieldColors = TextFieldDefaults.colors( disabledIndicatorColor = Color.Transparent, errorIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, errorContainerColor = MaterialTheme.colorScheme.errorContainer ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/Wave.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components import androidx.annotation.FloatRange import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.looker.kenko.ui.theme.KenkoTheme import kotlin.math.PI import kotlin.math.sin private const val WAVE_AMPLITUDE = 12F private const val WAVE_FREQUENCY = 0.055F private const val INITIAL_PHASE = 0F private const val FINAL_PHASE = (2 * PI).toFloat() @Composable fun Wave( modifier: Modifier = Modifier, strokeWidth: Float = 4F, amplitude: Float = WAVE_AMPLITUDE, frequency: Float = WAVE_FREQUENCY, color: Color = MaterialTheme.colorScheme.tertiary, ) { Canvas(modifier = modifier) { drawWave( strokeColor = color, strokeWidth = strokeWidth, amplitude = amplitude, frequency = frequency ) } } @Composable fun AnimatedWave( modifier: Modifier = Modifier, strokeWidth: Float = 4F, amplitude: Float = WAVE_AMPLITUDE, frequency: Float = WAVE_FREQUENCY, color: Color = MaterialTheme.colorScheme.tertiary, durationMillis: Int = 1_000, ) { val transition = rememberInfiniteTransition("Wave") val phase by transition.animateFloat( initialValue = INITIAL_PHASE, targetValue = FINAL_PHASE, animationSpec = infiniteRepeatable(tween(durationMillis, easing = LinearEasing)), label = "Phase" ) Canvas(modifier = modifier) { drawWave( strokeColor = color, strokeWidth = strokeWidth, amplitude = amplitude, frequency = frequency, phase = phase ) } } fun DrawScope.drawWave( strokeColor: Color, strokeWidth: Float = 4F, amplitude: Float = WAVE_AMPLITUDE, frequency: Float = WAVE_FREQUENCY, @FloatRange(INITIAL_PHASE.toDouble(), FINAL_PHASE.toDouble()) phase: Float = 0F, ) { val path = Path() val centerY = center.y path.moveTo(0F, amplitude * sin(phase) + centerY) for (x in 0..size.width.toInt()) { val y = amplitude * sin((frequency * x) + phase) path.lineTo(x.toFloat(), y + centerY) } drawPath( path = path, color = strokeColor, style = Stroke( width = strokeWidth, cap = StrokeCap.Round ) ) } @Preview(showBackground = true) @Composable private fun WavePreview() { KenkoTheme { Row(modifier = Modifier.fillMaxWidth()) { Text(text = "How the Wave?") Wave( modifier = Modifier .weight(1F) .height(24.dp) ) } } } @Preview(showBackground = true) @Composable private fun AnimatedWavePreview() { KenkoTheme { Row(modifier = Modifier.fillMaxWidth()) { Text(text = "How the Wave?") AnimatedWave( modifier = Modifier .weight(1F) .height(24.dp) ) } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/AddLarge.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp val AddLarge: ImageVector get() { if (_addLarge != null) { return _addLarge!! } _addLarge = icon( name = "AddLarge", viewPort = 960.0F to 960.0F, size = 120.dp to 120.dp, ) { path( fill = null, stroke = SolidColor(Color.Black), strokeLineWidth = 12.0f, strokeLineCap = StrokeCap.Round, strokeLineJoin = StrokeJoin.Round, strokeLineMiter = 0.0f, pathFillType = NonZero ) { moveTo(450.0f, 510.0f) lineTo(250.0f, 510.0f) quadTo(237.25f, 510.0f, 228.63f, 501.37f) quadTo(220.0f, 492.74f, 220.0f, 479.99f) quadTo(220.0f, 467.23f, 228.63f, 458.62f) quadTo(237.25f, 450.0f, 250.0f, 450.0f) lineTo(450.0f, 450.0f) lineTo(450.0f, 250.0f) quadTo(450.0f, 237.25f, 458.63f, 228.63f) quadTo(467.26f, 220.0f, 480.01f, 220.0f) quadTo(492.77f, 220.0f, 501.38f, 228.63f) quadTo(510.0f, 237.25f, 510.0f, 250.0f) lineTo(510.0f, 450.0f) lineTo(710.0f, 450.0f) quadTo(722.75f, 450.0f, 731.37f, 458.63f) quadTo(740.0f, 467.26f, 740.0f, 480.01f) quadTo(740.0f, 492.77f, 731.37f, 501.38f) quadTo(722.75f, 510.0f, 710.0f, 510.0f) lineTo(510.0f, 510.0f) lineTo(510.0f, 710.0f) quadTo(510.0f, 722.75f, 501.37f, 731.37f) quadTo(492.74f, 740.0f, 479.99f, 740.0f) quadTo(467.23f, 740.0f, 458.62f, 731.37f) quadTo(450.0f, 722.75f, 450.0f, 710.0f) lineTo(450.0f, 510.0f) close() } } return _addLarge!! } private var _addLarge: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/Arrow1.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path val Arrow1: ImageVector get() { if (_arrow1 != null) { return _arrow1!! } _arrow1 = icon( name = "Arrow1", viewPort = 57.0f to 57.0f ) { path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(11.4129f, 20.3807f) curveTo(11.8677f, 20.1523f, 12.2584f, 19.9533f, 12.4643f, 19.847f) curveTo(13.7156f, 19.1974f, 17.0194f, 17.961f, 19.0214f, 16.3208f) curveTo(20.5492f, 15.07f, 21.326f, 13.5811f, 20.3871f, 11.9663f) curveTo(19.5338f, 10.4987f, 17.6564f, 9.9976f, 15.3326f, 10.1916f) curveTo(10.3313f, 10.6092f, 3.217f, 14.1335f, 1.3427f, 15.7533f) curveTo(1.1381f, 15.9302f, 1.1153f, 16.2385f, 1.2922f, 16.443f) curveTo(1.4683f, 16.6467f, 1.7766f, 16.6695f, 1.9811f, 16.4925f) curveTo(3.7802f, 14.9386f, 10.6128f, 11.5664f, 15.4141f, 11.1654f) curveTo(17.2908f, 11.0089f, 18.8532f, 11.2725f, 19.5423f, 12.4582f) curveTo(20.223f, 13.6281f, 19.51f, 14.6577f, 18.4024f, 15.5645f) curveTo(16.4544f, 17.1611f, 13.2325f, 18.3475f, 12.0138f, 18.9794f) curveTo(11.25f, 19.3759f, 8.2942f, 20.8058f, 8.0703f, 20.9894f) curveTo(7.7899f, 21.2191f, 7.882f, 21.4884f, 7.9189f, 21.5745f) curveTo(7.9474f, 21.6413f, 8.1109f, 21.965f, 8.5402f, 21.8504f) curveTo(14.6622f, 20.2202f, 25.185f, 21.9561f, 33.3426f, 26.7524f) curveTo(41.4286f, 31.5065f, 47.1978f, 39.3f, 43.8692f, 49.8987f) curveTo(43.7888f, 50.1563f, 43.9323f, 50.4309f, 44.189f, 50.5121f) curveTo(44.4466f, 50.5925f, 44.7211f, 50.449f, 44.8015f, 50.1914f) curveTo(48.2889f, 39.0862f, 42.3096f, 30.8905f, 33.8386f, 25.91f) curveTo(26.6995f, 21.7122f, 17.7953f, 19.8224f, 11.4129f, 20.3807f) close() } path( fill = SolidColor(Color(0xFFE5E2DE)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(44.4335f, 49.4713f) curveTo(44.0066f, 48.4008f, 43.6156f, 47.3214f, 43.1053f, 46.2836f) curveTo(41.7461f, 43.5182f, 39.7482f, 40.9328f, 37.2431f, 39.1197f) curveTo(37.0235f, 38.9615f, 36.7183f, 39.0111f, 36.5602f, 39.229f) curveTo(36.402f, 39.4486f, 36.4516f, 39.7538f, 36.6695f, 39.912f) curveTo(39.0479f, 41.6312f, 40.9382f, 44.091f, 42.2275f, 46.7147f) curveTo(42.811f, 47.9029f, 43.2367f, 49.1478f, 43.742f, 50.3687f) curveTo(43.768f, 50.4293f, 43.9714f, 50.9665f, 44.0455f, 51.0681f) curveTo(44.2166f, 51.3057f, 44.4392f, 51.2948f, 44.5415f, 51.2765f) curveTo(44.6272f, 51.2623f, 44.7264f, 51.2248f, 44.823f, 51.1366f) curveTo(44.9019f, 51.0639f, 45.0192f, 50.8907f, 45.1267f, 50.6257f) curveTo(45.4737f, 49.7781f, 45.9994f, 47.6848f, 46.1221f, 47.3504f) curveTo(47.5276f, 43.5163f, 49.4788f, 40.1852f, 51.4454f, 36.6269f) curveTo(51.5751f, 36.3902f, 51.4896f, 36.0922f, 51.2539f, 35.9617f) curveTo(51.0181f, 35.8311f, 50.7209f, 35.9175f, 50.5904f, 36.1533f) curveTo(48.5986f, 39.7565f, 46.6274f, 43.1325f, 45.2054f, 47.0134f) curveTo(45.1203f, 47.243f, 44.7192f, 48.5406f, 44.4335f, 49.4713f) close() } } return _arrow1!! } private var _arrow1: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/Arrow2.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path val Arrow2: ImageVector get() { if (_arrow2 != null) { return _arrow2!! } _arrow2 = icon( name = "Arrow2", viewPort = 60F to 60F, ) { path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(25.0814f, 0.6872f) curveTo(29.776f, 4.8349f, 32.4145f, 10.6669f, 33.6055f, 17.025f) curveTo(35.4039f, 26.6272f, 33.9028f, 37.4312f, 31.1225f, 45.5342f) curveTo(29.3757f, 50.6271f, 26.4914f, 53.4886f, 23.7002f, 58.0623f) curveTo(23.5873f, 58.2476f, 23.6457f, 58.4893f, 23.831f, 58.6023f) curveTo(24.0153f, 58.7153f, 24.258f, 58.6568f, 24.37f, 58.4715f) curveTo(27.1939f, 53.8443f, 30.098f, 50.9412f, 31.8656f, 45.7889f) curveTo(34.6845f, 37.5729f, 36.2005f, 26.6173f, 34.3774f, 16.8804f) curveTo(33.1537f, 10.3468f, 30.426f, 4.3612f, 25.6016f, 0.0987f) curveTo(25.4391f, -0.045f, 25.1914f, -0.0301f, 25.0477f, 0.1324f) curveTo(24.9041f, 0.2949f, 24.9189f, 0.5436f, 25.0814f, 0.6872f) close() } path( fill = SolidColor(Color(0xFFE5E2DE)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(23.9498f, 59.5777f) curveTo(23.9607f, 59.5618f, 24.031f, 59.4548f, 24.0994f, 59.3716f) curveTo(24.3808f, 59.0248f, 24.9287f, 58.463f, 25.6619f, 57.7952f) curveTo(28.1241f, 55.5579f, 32.6671f, 52.1395f, 36.4115f, 51.7967f) curveTo(36.6275f, 51.7769f, 36.786f, 51.5856f, 36.7662f, 51.3696f) curveTo(36.7464f, 51.1536f, 36.5561f, 50.9941f, 36.3401f, 51.0139f) curveTo(32.4442f, 51.3706f, 27.6951f, 54.8861f, 25.1338f, 57.2145f) curveTo(24.791f, 57.5266f, 24.4858f, 57.8169f, 24.2282f, 58.0756f) curveTo(24.4502f, 56.9827f, 24.6998f, 55.8977f, 24.9624f, 54.8128f) curveTo(25.7194f, 51.6897f, 25.7273f, 48.8311f, 25.0823f, 45.6584f) curveTo(25.0397f, 45.4464f, 24.8316f, 45.3087f, 24.6196f, 45.3523f) curveTo(24.4075f, 45.3949f, 24.2698f, 45.603f, 24.3134f, 45.815f) curveTo(24.9337f, 48.8687f, 24.9277f, 51.6213f, 24.1995f, 54.6275f) curveTo(23.8567f, 56.0453f, 23.5356f, 57.4652f, 23.2671f, 58.8989f) curveTo(23.2403f, 59.0416f, 23.175f, 59.4132f, 23.174f, 59.4667f) curveTo(23.168f, 59.7719f, 23.4157f, 59.8472f, 23.4712f, 59.861f) curveTo(23.498f, 59.868f, 23.8349f, 59.9344f, 23.9498f, 59.5777f) close() moveTo(23.2116f, 59.3101f) curveTo(23.2086f, 59.32f, 23.2047f, 59.3299f, 23.2017f, 59.3408f) curveTo(23.2037f, 59.3319f, 23.2076f, 59.322f, 23.2116f, 59.3101f) close() } } return _arrow2!! } private var _arrow2: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/Arrow3.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.group import androidx.compose.ui.graphics.vector.path val Arrow3: ImageVector get() { if (_arrow3 != null) { return _arrow3!! } _arrow3 = icon( name = "Arrow3", viewPort = 80F to 80F, ) { group { path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(37.615f, 26.0972f) curveTo(28.649f, 27.4448f, 19.4581f, 30.3978f, 12.613f, 36.3343f) curveTo(10.1222f, 38.4941f, 8.2614f, 41.0838f, 6.3325f, 43.7179f) curveTo(6.197f, 43.9024f, 6.2371f, 44.1632f, 6.4222f, 44.2996f) curveTo(6.6082f, 44.4353f, 6.8691f, 44.3953f, 7.0048f, 44.2092f) curveTo(8.8967f, 41.6263f, 10.716f, 39.0817f, 13.1587f, 36.9637f) curveTo(19.9066f, 31.1114f, 28.9792f, 28.2256f, 37.8172f, 26.9085f) lineTo(37.8484f, 27.0428f) curveTo(38.1835f, 28.5022f, 39.6864f, 29.8681f, 41.7434f, 30.9401f) curveTo(44.6917f, 32.4771f, 48.7588f, 33.451f, 51.7441f, 33.4322f) curveTo(53.0856f, 33.4235f, 54.2157f, 33.2089f, 54.9591f, 32.7818f) curveTo(55.539f, 32.4491f, 55.9037f, 31.9876f, 56.016f, 31.4061f) curveTo(56.2544f, 30.1667f, 55.3591f, 29.0333f, 53.7589f, 28.143f) curveTo(50.4125f, 26.28f, 43.9984f, 25.3394f, 41.1348f, 25.6454f) curveTo(40.2406f, 25.7413f, 39.3405f, 25.8512f, 38.4381f, 25.978f) curveTo(38.3721f, 25.723f, 38.3086f, 25.4729f, 38.2579f, 25.227f) curveTo(38.1153f, 24.5259f, 38.0776f, 23.8473f, 38.4269f, 23.0995f) curveTo(38.9727f, 21.9346f, 39.9983f, 21.0312f, 41.2058f, 20.3163f) curveTo(43.0909f, 19.2019f, 45.4227f, 18.5526f, 47.2047f, 18.1611f) curveTo(57.1293f, 15.9813f, 65.2005f, 19.09f, 71.5946f, 23.4982f) curveTo(78.0512f, 27.9512f, 82.8103f, 33.7274f, 86.0504f, 36.8085f) curveTo(86.2168f, 36.9673f, 86.4801f, 36.9597f, 86.6389f, 36.7933f) curveTo(86.7977f, 36.627f, 86.791f, 36.363f, 86.6238f, 36.2048f) curveTo(83.363f, 33.1036f, 78.567f, 27.2948f, 72.0666f, 22.813f) curveTo(65.5037f, 18.2864f, 57.2141f, 15.1102f, 47.0264f, 17.3469f) curveTo(45.1701f, 17.7545f, 42.745f, 18.4385f, 40.7821f, 19.5991f) curveTo(39.4235f, 20.4032f, 38.2865f, 21.4357f, 37.6725f, 22.7468f) curveTo(37.1219f, 23.9236f, 37.3291f, 24.9563f, 37.615f, 26.0972f) close() moveTo(38.6449f, 26.79f) lineTo(38.6598f, 26.857f) curveTo(38.9525f, 28.1327f, 40.3295f, 29.2641f, 42.1283f, 30.2011f) curveTo(44.9614f, 31.6781f, 48.869f, 32.6171f, 51.7392f, 32.5985f) curveTo(52.7232f, 32.5928f, 53.5805f, 32.4753f, 54.218f, 32.2173f) curveTo(54.7436f, 32.0039f, 55.1111f, 31.7009f, 55.1973f, 31.2487f) curveTo(55.2807f, 30.8221f, 55.1424f, 30.4253f, 54.8576f, 30.0565f) curveTo(54.524f, 29.6228f, 54.0005f, 29.2307f, 53.3544f, 28.8706f) curveTo(50.1387f, 27.0807f, 43.9751f, 26.1792f, 41.2235f, 26.4736f) curveTo(40.369f, 26.5657f, 39.5088f, 26.6703f, 38.6449f, 26.79f) close() } path( fill = SolidColor(Color(0xFFE5E2DE)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(6.2642f, 45.3833f) curveTo(6.1855f, 45.2188f, 6.1475f, 44.9661f, 6.1098f, 44.6938f) curveTo(6.0193f, 44.0454f, 6.0141f, 43.2861f, 5.997f, 42.9283f) curveTo(5.9855f, 42.6981f, 5.7895f, 42.5218f, 5.5602f, 42.5326f) curveTo(5.33f, 42.5441f, 5.153f, 42.7392f, 5.1645f, 42.9694f) curveTo(5.1859f, 43.4077f, 5.1948f, 44.4196f, 5.3423f, 45.1527f) curveTo(5.4217f, 45.5459f, 5.5511f, 45.8671f, 5.7114f, 46.047f) curveTo(5.9837f, 46.3538f, 6.3958f, 46.3631f, 6.8988f, 45.9649f) curveTo(7.4935f, 45.4934f, 8.3896f, 44.3417f, 9.6069f, 43.9969f) curveTo(9.8278f, 43.9348f, 9.9566f, 43.7032f, 9.8936f, 43.483f) curveTo(9.8309f, 43.2612f, 9.6008f, 43.1326f, 9.379f, 43.1953f) curveTo(8.2187f, 43.5247f, 7.2975f, 44.481f, 6.6455f, 45.0822f) curveTo(6.5214f, 45.1963f, 6.3607f, 45.3147f, 6.2642f, 45.3833f) close() } } } return _arrow3!! } private var _arrow3: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/Arrow4.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.group import androidx.compose.ui.graphics.vector.path val Arrow4: ImageVector get() { if (_arrow4 != null) { return _arrow4!! } _arrow4 = icon( name = "Arrow4", viewPort = 62.0f to 62.0f ) { group { path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(36.5224f, 31.7713f) curveTo(36.2713f, 31.6744f, 35.9937f, 31.621f, 35.7165f, 31.5679f) curveTo(34.2685f, 31.2034f, 32.5932f, 30.7571f, 31.07f, 30.8859f) curveTo(29.0619f, 31.0721f, 27.8076f, 31.9092f, 27.0907f, 33.0823f) curveTo(26.0832f, 34.7341f, 26.2413f, 37.1742f, 27.2523f, 39.2468f) curveTo(28.2481f, 41.3099f, 30.036f, 43.0278f, 32.0984f, 43.4159f) curveTo(33.5042f, 43.694f, 35.0671f, 43.3498f, 36.6209f, 42.0393f) curveTo(38.3908f, 40.5636f, 38.9983f, 38.4816f, 38.8949f, 36.2557f) curveTo(38.8693f, 35.3992f, 38.7077f, 34.5778f, 38.4969f, 33.7257f) curveTo(39.2812f, 34.0357f, 40.0726f, 34.3498f, 40.8737f, 34.6703f) curveTo(43.4475f, 35.7983f, 45.98f, 37.2605f, 48.276f, 38.8754f) curveTo(51.3393f, 40.9696f, 53.9423f, 43.4971f, 56.0927f, 46.5219f) curveTo(56.3073f, 46.776f, 56.6957f, 46.8385f, 56.9681f, 46.6485f) curveTo(57.2401f, 46.4583f, 57.3011f, 46.0763f, 57.1134f, 45.779f) curveTo(54.8756f, 42.6395f, 52.1814f, 39.9949f, 48.9946f, 37.8234f) curveTo(46.6025f, 36.2086f, 44.0252f, 34.6586f, 41.3557f, 33.5309f) curveTo(40.2426f, 33.0755f, 39.1481f, 32.6316f, 38.0378f, 32.238f) curveTo(37.3786f, 30.4452f, 36.4424f, 28.7795f, 35.5302f, 27.549f) curveTo(29.3312f, 19.4728f, 15.9851f, 16.5965f, 6.2282f, 15.9031f) curveTo(5.8908f, 15.8723f, 5.6042f, 16.1139f, 5.5779f, 16.4577f) curveTo(5.552f, 16.8017f, 5.8163f, 17.0869f, 6.1537f, 17.1176f) curveTo(15.5944f, 17.7935f, 28.5453f, 20.4825f, 34.5384f, 28.31f) curveTo(35.2431f, 29.2307f, 35.9489f, 30.4523f, 36.5224f, 31.7713f) close() moveTo(37.0563f, 33.2458f) curveTo(36.5403f, 33.0435f, 36.0005f, 32.886f, 35.4349f, 32.7727f) curveTo(34.0876f, 32.4711f, 32.5739f, 32.0054f, 31.1918f, 32.1024f) curveTo(29.6789f, 32.2378f, 28.7f, 32.8273f, 28.169f, 33.6962f) curveTo(27.3199f, 35.0869f, 27.5512f, 37.0323f, 28.3801f, 38.6909f) curveTo(29.1967f, 40.402f, 30.6168f, 41.8899f, 32.3359f, 42.1836f) curveTo(33.4326f, 42.3886f, 34.618f, 42.1084f, 35.8102f, 41.1123f) curveTo(37.2837f, 39.8717f, 37.7553f, 38.1254f, 37.6522f, 36.3199f) curveTo(37.6182f, 35.278f, 37.3951f, 34.238f, 37.0563f, 33.2458f) close() } path( fill = SolidColor(Color(0xFFE5E2DE)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(5.8248f, 16.3705f) curveTo(6.1121f, 16.2498f, 6.475f, 16.1764f, 6.8383f, 16.1032f) curveTo(7.5472f, 15.9459f, 8.2556f, 15.848f, 8.7167f, 15.6558f) curveTo(11.5221f, 14.5271f, 14.8724f, 13.1385f, 17.2022f, 11.1721f) curveTo(17.4534f, 10.9689f, 17.4907f, 10.572f, 17.2684f, 10.3129f) curveTo(17.0464f, 10.0542f, 16.6689f, 9.9983f, 16.3906f, 10.2447f) curveTo(14.1737f, 12.1014f, 10.9604f, 13.3959f, 8.2428f, 14.5195f) curveTo(7.7618f, 14.6993f, 6.9039f, 14.8233f, 6.1841f, 14.9739f) curveTo(5.5596f, 15.124f, 5.0123f, 15.3225f, 4.6589f, 15.642f) curveTo(4.4463f, 15.8093f, 4.2899f, 16.2519f, 4.6142f, 16.8148f) curveTo(5.0874f, 17.5908f, 6.8121f, 19.0891f, 6.9703f, 19.248f) curveTo(8.5771f, 20.8524f, 9.9584f, 22.3158f, 10.118f, 24.697f) curveTo(10.1536f, 25.0194f, 10.4562f, 25.2684f, 10.7716f, 25.2854f) curveTo(11.1139f, 25.2592f, 11.3816f, 24.946f, 11.3456f, 24.6233f) curveTo(11.1551f, 21.9227f, 9.6835f, 20.1627f, 7.8412f, 18.3511f) curveTo(7.7048f, 18.2658f, 6.6689f, 17.3784f, 6.0359f, 16.6226f) curveTo(5.9769f, 16.5257f, 5.8824f, 16.4666f, 5.8248f, 16.3705f) close() } } } return _arrow4!! } private var _arrow4: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/ArrowOutwardLarge.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp val ArrowOutwardLarge: ImageVector get() { if (_arrowOutwardLarge != null) { return _arrowOutwardLarge!! } _arrowOutwardLarge = icon( name = "ArrowOutwardLarge", viewPort = 960.0F to 960.0F, size = 120.dp to 120.dp, ) { path( fill = null, stroke = SolidColor(Color.Black), strokeLineWidth = 12.0f, strokeLineCap = StrokeCap.Round, strokeLineJoin = StrokeJoin.Round, strokeLineMiter = 0.0f, pathFillType = NonZero ) { moveTo(645.77f, 312.15f) lineTo(272.46f, 685.08f) quadTo(264.15f, 693.38f, 251.58f, 693.19f) quadTo(239.0f, 693.0f, 230.69f, 684.69f) quadTo(222.39f, 676.38f, 222.39f, 664.0f) quadTo(222.39f, 651.62f, 230.69f, 643.31f) lineTo(603.62f, 270.0f) lineTo(275.77f, 270.0f) quadTo(263.02f, 270.0f, 254.39f, 261.37f) quadTo(245.77f, 252.74f, 245.77f, 239.99f) quadTo(245.77f, 227.23f, 254.39f, 218.62f) quadTo(263.02f, 210.0f, 275.77f, 210.0f) lineTo(669.61f, 210.0f) quadTo(684.98f, 210.0f, 695.37f, 220.39f) quadTo(705.77f, 230.79f, 705.77f, 246.15f) lineTo(705.77f, 640.0f) quadTo(705.77f, 652.75f, 697.14f, 661.37f) quadTo(688.51f, 670.0f, 675.76f, 670.0f) quadTo(663.0f, 670.0f, 654.38f, 661.37f) quadTo(645.77f, 652.75f, 645.77f, 640.0f) lineTo(645.77f, 312.15f) close() } } return _arrowOutwardLarge!! } private var _arrowOutwardLarge: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/Cloud.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path val Cloud: ImageVector get() { if (_cloud != null) { return _cloud!! } _cloud = icon( name = "Cloud", viewPort = 118F to 100F ) { path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(69.705f, 23.878f) curveTo(67.542f, 22.111f, 65.151f, 20.573f, 62.675f, 19.323f) curveTo(57.833f, 16.879f, 50.563f, 14.678f, 43.832f, 14.616f) curveTo(39.218f, 14.573f, 34.86f, 15.541f, 31.64f, 18.035f) curveTo(25.509f, 22.783f, 24.246f, 30.986f, 25.998f, 39.508f) curveTo(27.276f, 45.724f, 30.145f, 52.102f, 33.769f, 57.338f) curveTo(29.653f, 54.981f, 24.33f, 52.795f, 19.161f, 52.257f) curveTo(13.367f, 51.649f, 7.773f, 53.089f, 4.122f, 58.449f) curveTo(-3.509f, 69.652f, 0.312f, 80.491f, 8.602f, 88.315f) curveTo(16.746f, 96.0f, 29.235f, 100.717f, 39.092f, 99.911f) curveTo(39.613f, 99.866f, 40.001f, 99.408f, 39.958f, 98.885f) curveTo(39.916f, 98.364f, 39.458f, 97.974f, 38.938f, 98.019f) curveTo(29.546f, 98.784f, 17.66f, 94.257f, 9.901f, 86.933f) curveTo(2.287f, 79.747f, -1.322f, 69.807f, 5.686f, 59.521f) curveTo(8.902f, 54.801f, 13.861f, 53.608f, 18.965f, 54.143f) curveTo(24.275f, 54.699f, 29.749f, 57.101f, 33.766f, 59.537f) curveTo(34.854f, 60.195f, 35.952f, 60.847f, 37.041f, 61.514f) curveTo(39.961f, 64.795f, 43.162f, 67.266f, 46.306f, 68.462f) curveTo(47.087f, 68.756f, 47.446f, 68.244f, 47.54f, 68.095f) curveTo(47.668f, 67.893f, 47.792f, 67.567f, 47.585f, 67.146f) curveTo(47.546f, 67.067f, 47.451f, 66.918f, 47.28f, 66.754f) curveTo(47.048f, 66.532f, 46.53f, 66.124f, 46.315f, 65.925f) curveTo(45.428f, 65.096f, 44.455f, 64.333f, 43.498f, 63.593f) curveTo(41.834f, 62.302f, 40.067f, 61.154f, 38.274f, 60.049f) curveTo(38.017f, 59.755f, 37.763f, 59.458f, 37.512f, 59.154f) curveTo(33.015f, 53.703f, 29.328f, 46.303f, 27.853f, 39.126f) curveTo(26.259f, 31.37f, 27.219f, 23.858f, 32.799f, 19.537f) curveTo(35.703f, 17.288f, 39.652f, 16.476f, 43.815f, 16.514f) curveTo(50.248f, 16.574f, 57.195f, 18.682f, 61.823f, 21.019f) curveTo(64.65f, 22.446f, 67.362f, 24.266f, 69.719f, 26.388f) curveTo(70.501f, 27.092f, 71.215f, 27.892f, 71.899f, 28.692f) curveTo(71.974f, 28.78f, 72.077f, 28.91f, 72.193f, 29.059f) curveTo(72.491f, 29.506f, 72.807f, 29.936f, 73.14f, 30.349f) curveTo(73.315f, 30.566f, 73.435f, 30.644f, 73.457f, 30.658f) curveTo(74.03f, 31.025f, 74.436f, 30.717f, 74.609f, 30.557f) curveTo(74.661f, 30.51f, 75.18f, 30.0f, 74.758f, 29.33f) curveTo(74.672f, 29.195f, 74.137f, 28.468f, 73.711f, 27.921f) curveTo(70.552f, 23.151f, 69.568f, 16.438f, 70.964f, 10.989f) curveTo(71.849f, 7.534f, 73.695f, 4.582f, 76.621f, 3.042f) curveTo(78.567f, 2.018f, 80.98f, 1.628f, 83.876f, 2.094f) curveTo(90.859f, 3.216f, 96.708f, 7.21f, 101.914f, 12.126f) curveTo(107.239f, 17.156f, 111.889f, 23.156f, 116.346f, 28.176f) curveTo(116.693f, 28.568f, 117.292f, 28.603f, 117.683f, 28.254f) curveTo(118.073f, 27.906f, 118.108f, 27.306f, 117.761f, 26.914f) curveTo(113.27f, 21.855f, 108.579f, 15.814f, 103.213f, 10.745f) curveTo(97.727f, 5.563f, 91.535f, 1.402f, 84.176f, 0.219f) curveTo(80.807f, -0.322f, 78.004f, 0.17f, 75.74f, 1.361f) curveTo(72.353f, 3.145f, 70.153f, 6.517f, 69.129f, 10.517f) curveTo(68.059f, 14.696f, 68.281f, 19.553f, 69.705f, 23.878f) close() } } return _cloud!! } private var _cloud: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/Colony.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.group import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp val Colony: ImageVector get() { if (_colony != null) { return _colony!! } _colony = icon( name = "Colony", viewPort = 500F to 500F, size = 150.dp to 150.dp, ) { group { path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(174.4f, 87.9f) curveTo(149.1f, 95.2f, 126.0f, 109.2f, 107.8f, 128.2f) curveTo(102.7f, 133.5f, 98.0f, 139.3f, 93.7f, 145.3f) curveTo(93.4f, 145.7f, 93.7f, 147.3f, 93.7f, 147.8f) curveTo(93.7f, 148.4f, 93.7f, 149.0f, 93.7f, 149.6f) curveTo(93.7f, 149.7f, 93.8f, 150.2f, 93.7f, 150.3f) curveTo(108.9f, 128.8f, 129.8f, 111.5f, 153.7f, 100.6f) curveTo(160.4f, 97.5f, 167.4f, 95.0f, 174.5f, 92.9f) curveTo(174.6f, 92.9f, 174.5f, 90.6f, 174.5f, 90.4f) curveTo(174.4f, 90.2f, 174.3f, 88.0f, 174.4f, 87.9f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(81.9f, 169.0f) curveTo(73.6f, 187.0f, 69.0f, 206.1f, 67.2f, 225.8f) curveTo(66.7f, 231.4f, 66.4f, 237.1f, 66.1f, 242.8f) curveTo(66.1f, 243.8f, 66.1f, 244.9f, 66.1f, 245.9f) curveTo(66.1f, 246.4f, 66.1f, 246.9f, 66.1f, 247.4f) curveTo(66.1f, 247.4f, 66.1f, 247.9f, 66.1f, 247.7f) curveTo(66.6f, 237.5f, 67.3f, 227.2f, 68.9f, 217.1f) curveTo(70.4f, 207.6f, 72.7f, 198.2f, 75.9f, 189.1f) curveTo(77.7f, 184.0f, 79.7f, 179.0f, 82.0f, 174.1f) curveTo(82.3f, 173.5f, 82.0f, 172.2f, 82.0f, 171.6f) curveTo(82.0f, 171.0f, 82.0f, 170.4f, 82.0f, 169.8f) curveTo(81.9f, 169.6f, 81.8f, 169.2f, 81.9f, 169.0f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(67.6f, 271.1f) curveTo(68.8f, 291.5f, 73.3f, 311.7f, 80.8f, 330.8f) curveTo(83.0f, 336.2f, 85.4f, 341.6f, 88.0f, 346.8f) curveTo(88.0f, 346.7f, 88.0f, 346.1f, 88.0f, 346.1f) curveTo(88.0f, 345.5f, 88.0f, 344.9f, 88.0f, 344.3f) curveTo(88.0f, 343.7f, 88.0f, 343.1f, 88.0f, 342.5f) curveTo(88.0f, 342.3f, 88.1f, 341.9f, 88.0f, 341.8f) curveTo(78.8f, 323.6f, 72.5f, 304.0f, 69.4f, 283.9f) curveTo(68.5f, 278.1f, 67.9f, 272.2f, 67.5f, 266.3f) curveTo(67.5f, 266.0f, 67.5f, 266.7f, 67.5f, 266.6f) curveTo(67.5f, 267.1f, 67.5f, 267.6f, 67.5f, 268.1f) curveTo(67.5f, 269.0f, 67.5f, 270.1f, 67.6f, 271.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(100.0f, 364.4f) curveTo(106.9f, 371.8f, 113.8f, 379.2f, 120.7f, 386.6f) curveTo(124.2f, 390.3f, 127.6f, 394.1f, 131.1f, 397.8f) curveTo(134.3f, 401.2f, 137.5f, 404.4f, 141.3f, 407.1f) curveTo(145.6f, 410.1f, 150.2f, 412.5f, 154.9f, 414.8f) curveTo(154.8f, 414.7f, 154.9f, 412.5f, 154.9f, 412.3f) curveTo(154.9f, 412.0f, 155.1f, 409.9f, 154.9f, 409.8f) curveTo(150.8f, 407.7f, 146.8f, 405.7f, 143.0f, 403.2f) curveTo(139.1f, 400.6f, 135.6f, 397.5f, 132.4f, 394.2f) curveTo(125.5f, 387.0f, 118.7f, 379.6f, 111.9f, 372.2f) curveTo(107.9f, 368.0f, 104.0f, 363.7f, 100.0f, 359.5f) curveTo(100.2f, 359.8f, 100.0f, 361.6f, 100.0f, 362.0f) curveTo(100.0f, 362.6f, 100.0f, 363.2f, 100.0f, 363.8f) curveTo(100.0f, 363.8f, 99.9f, 364.3f, 100.0f, 364.4f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(191.7f, 431.1f) curveTo(214.6f, 436.3f, 237.7f, 441.5f, 261.3f, 441.9f) curveTo(282.7f, 442.3f, 303.9f, 438.2f, 323.3f, 429.1f) curveTo(333.6f, 424.2f, 343.3f, 418.1f, 352.1f, 410.8f) curveTo(352.4f, 410.6f, 352.1f, 408.7f, 352.1f, 408.3f) curveTo(352.1f, 408.0f, 351.8f, 406.0f, 352.1f, 405.8f) curveTo(336.4f, 418.8f, 317.8f, 428.2f, 298.1f, 433.1f) curveTo(275.9f, 438.5f, 253.0f, 437.6f, 230.6f, 434.0f) curveTo(217.5f, 431.9f, 204.6f, 429.0f, 191.8f, 426.1f) curveTo(192.0f, 426.2f, 191.3f, 431.0f, 191.7f, 431.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(380.0f, 389.1f) curveTo(388.1f, 379.2f, 396.3f, 369.4f, 404.4f, 359.5f) curveTo(408.2f, 354.8f, 411.8f, 350.0f, 414.6f, 344.7f) curveTo(417.5f, 339.3f, 419.5f, 333.5f, 421.5f, 327.7f) curveTo(423.8f, 320.9f, 426.1f, 314.0f, 428.5f, 307.1f) curveTo(428.7f, 306.4f, 428.5f, 305.3f, 428.5f, 304.6f) curveTo(428.5f, 303.9f, 428.2f, 302.8f, 428.5f, 302.1f) curveTo(426.4f, 308.2f, 424.4f, 314.3f, 422.3f, 320.4f) curveTo(420.3f, 326.2f, 418.4f, 332.0f, 415.7f, 337.6f) curveTo(410.3f, 348.7f, 401.7f, 357.9f, 393.9f, 367.3f) curveTo(389.3f, 372.9f, 384.7f, 378.5f, 380.1f, 384.1f) curveTo(379.8f, 384.4f, 380.1f, 386.1f, 380.1f, 386.6f) curveTo(380.1f, 387.2f, 380.1f, 387.8f, 380.1f, 388.4f) curveTo(380.0f, 388.5f, 380.1f, 389.0f, 380.0f, 389.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(431.5f, 274.8f) curveTo(433.9f, 261.5f, 434.4f, 247.9f, 433.6f, 234.4f) curveTo(432.7f, 221.1f, 430.1f, 207.9f, 425.7f, 195.3f) curveTo(423.2f, 188.2f, 420.2f, 181.3f, 416.6f, 174.6f) curveTo(416.6f, 174.7f, 416.6f, 175.3f, 416.6f, 175.3f) curveTo(416.6f, 175.9f, 416.6f, 176.5f, 416.6f, 177.1f) curveTo(416.6f, 177.7f, 416.6f, 178.3f, 416.6f, 178.9f) curveTo(416.6f, 179.1f, 416.5f, 179.5f, 416.6f, 179.6f) curveTo(422.3f, 190.3f, 426.7f, 201.8f, 429.6f, 213.6f) curveTo(431.0f, 219.4f, 432.1f, 225.2f, 432.8f, 231.1f) curveTo(433.2f, 234.2f, 433.5f, 237.4f, 433.6f, 240.5f) curveTo(433.7f, 241.9f, 433.7f, 243.4f, 433.8f, 244.8f) curveTo(433.8f, 245.2f, 433.8f, 245.7f, 433.8f, 246.1f) curveTo(433.8f, 247.0f, 433.8f, 244.8f, 433.8f, 246.3f) curveTo(433.8f, 247.2f, 433.8f, 248.2f, 433.7f, 249.1f) curveTo(433.4f, 256.0f, 432.7f, 262.9f, 431.4f, 269.7f) curveTo(431.2f, 271.4f, 431.8f, 273.2f, 431.5f, 274.8f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(402.4f, 154.0f) curveTo(395.8f, 131.4f, 378.9f, 114.1f, 361.1f, 99.7f) curveTo(355.7f, 95.4f, 350.2f, 91.2f, 344.7f, 87.1f) curveTo(344.9f, 87.2f, 344.7f, 89.3f, 344.7f, 89.6f) curveTo(344.7f, 89.9f, 344.4f, 91.9f, 344.7f, 92.1f) curveTo(363.5f, 106.2f, 383.0f, 120.9f, 395.0f, 141.7f) curveTo(398.1f, 147.2f, 400.7f, 152.9f, 402.5f, 159.0f) curveTo(402.3f, 158.3f, 402.5f, 157.2f, 402.5f, 156.5f) curveTo(402.4f, 155.7f, 402.6f, 154.7f, 402.4f, 154.0f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(317.0f, 77.7f) curveTo(296.8f, 69.6f, 275.7f, 63.7f, 254.2f, 60.3f) curveTo(248.2f, 59.3f, 242.1f, 58.6f, 236.0f, 58.0f) curveTo(236.1f, 58.0f, 235.8f, 63.0f, 236.0f, 63.0f) curveTo(257.7f, 65.0f, 279.1f, 69.5f, 299.7f, 76.3f) curveTo(305.5f, 78.2f, 311.2f, 80.3f, 316.9f, 82.6f) curveTo(316.8f, 82.6f, 316.9f, 80.3f, 316.9f, 80.1f) curveTo(317.0f, 80.0f, 317.2f, 77.8f, 317.0f, 77.7f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(198.0f, 64.7f) curveTo(187.5f, 66.0f, 177.0f, 68.3f, 166.9f, 71.7f) curveTo(161.9f, 73.3f, 157.0f, 75.2f, 152.2f, 77.3f) curveTo(148.1f, 79.1f, 144.1f, 81.0f, 140.4f, 83.5f) curveTo(136.4f, 86.3f, 132.9f, 89.8f, 130.8f, 94.2f) curveTo(130.5f, 94.8f, 130.8f, 96.1f, 130.8f, 96.7f) curveTo(130.8f, 97.3f, 130.8f, 97.9f, 130.8f, 98.5f) curveTo(130.8f, 98.7f, 130.9f, 99.1f, 130.8f, 99.2f) curveTo(134.6f, 91.1f, 142.7f, 86.5f, 150.5f, 83.0f) curveTo(160.0f, 78.7f, 170.0f, 75.3f, 180.2f, 72.9f) curveTo(186.0f, 71.5f, 192.0f, 70.5f, 197.9f, 69.7f) curveTo(198.2f, 69.6f, 197.8f, 64.7f, 198.0f, 64.7f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(191.1f, 90.5f) curveTo(208.4f, 88.9f, 225.9f, 88.6f, 243.3f, 89.5f) curveTo(243.2f, 89.5f, 243.4f, 84.5f, 243.3f, 84.5f) curveTo(225.9f, 83.5f, 208.5f, 83.8f, 191.1f, 85.5f) curveTo(191.0f, 85.5f, 191.3f, 90.5f, 191.1f, 90.5f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(271.0f, 91.3f) curveTo(295.9f, 92.8f, 320.1f, 101.4f, 340.5f, 115.8f) curveTo(346.2f, 119.9f, 351.6f, 124.4f, 356.7f, 129.3f) curveTo(356.5f, 129.1f, 356.7f, 127.2f, 356.7f, 126.8f) curveTo(356.7f, 126.2f, 356.7f, 125.6f, 356.7f, 125.0f) curveTo(356.7f, 124.9f, 356.8f, 124.3f, 356.7f, 124.3f) curveTo(338.9f, 106.9f, 316.2f, 94.7f, 291.9f, 89.3f) curveTo(285.0f, 87.8f, 278.1f, 86.8f, 271.1f, 86.4f) curveTo(271.0f, 86.3f, 270.8f, 91.2f, 271.0f, 91.3f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(370.7f, 142.6f) curveTo(383.6f, 162.5f, 396.6f, 182.7f, 404.3f, 205.2f) curveTo(406.4f, 211.4f, 408.1f, 217.8f, 409.3f, 224.3f) curveTo(409.0f, 222.7f, 409.6f, 220.9f, 409.3f, 219.3f) curveTo(405.1f, 195.8f, 393.9f, 174.5f, 381.4f, 154.4f) curveTo(377.9f, 148.7f, 374.3f, 143.1f, 370.7f, 137.5f) curveTo(370.7f, 137.6f, 370.7f, 138.2f, 370.7f, 138.2f) curveTo(370.7f, 138.8f, 370.7f, 139.4f, 370.7f, 140.0f) curveTo(370.7f, 140.6f, 370.7f, 141.2f, 370.7f, 141.8f) curveTo(370.7f, 142.0f, 370.6f, 142.4f, 370.7f, 142.6f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(411.5f, 247.7f) curveTo(412.4f, 258.1f, 412.2f, 267.6f, 411.0f, 278.1f) curveTo(409.9f, 288.0f, 408.0f, 297.7f, 405.4f, 307.3f) curveTo(405.2f, 308.0f, 405.4f, 309.1f, 405.4f, 309.8f) curveTo(405.4f, 310.5f, 405.6f, 311.6f, 405.4f, 312.3f) curveTo(410.3f, 294.4f, 412.3f, 276.2f, 412.1f, 257.7f) curveTo(412.1f, 252.7f, 411.9f, 247.7f, 411.4f, 242.7f) curveTo(411.7f, 244.4f, 411.4f, 246.1f, 411.5f, 247.7f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(399.4f, 328.8f) curveTo(391.3f, 344.6f, 382.9f, 360.7f, 369.6f, 372.9f) curveTo(369.3f, 373.1f, 369.6f, 375.0f, 369.6f, 375.4f) curveTo(369.6f, 376.0f, 369.6f, 376.6f, 369.6f, 377.2f) curveTo(369.6f, 377.3f, 369.7f, 377.9f, 369.6f, 377.9f) curveTo(382.9f, 365.8f, 391.3f, 349.6f, 399.4f, 333.8f) curveTo(399.7f, 333.3f, 399.4f, 331.9f, 399.4f, 331.3f) curveTo(399.4f, 330.7f, 399.4f, 330.1f, 399.4f, 329.5f) curveTo(399.5f, 329.3f, 399.4f, 328.9f, 399.4f, 328.8f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(351.4f, 388.9f) curveTo(330.9f, 399.1f, 309.2f, 406.8f, 286.9f, 411.7f) curveTo(280.6f, 413.1f, 274.3f, 414.3f, 267.9f, 415.2f) curveTo(267.6f, 415.2f, 268.1f, 420.2f, 267.9f, 420.2f) curveTo(290.5f, 416.8f, 312.7f, 410.6f, 333.9f, 401.9f) curveTo(339.9f, 399.4f, 345.7f, 396.7f, 351.5f, 393.9f) curveTo(351.7f, 393.8f, 351.5f, 391.7f, 351.5f, 391.4f) curveTo(351.4f, 391.2f, 351.2f, 389.0f, 351.4f, 388.9f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(237.7f, 413.3f) curveTo(216.9f, 411.1f, 196.5f, 405.4f, 177.6f, 396.5f) curveTo(172.2f, 394.0f, 167.0f, 391.2f, 161.9f, 388.1f) curveTo(162.1f, 388.2f, 161.9f, 390.3f, 161.9f, 390.6f) curveTo(161.9f, 390.9f, 161.7f, 393.0f, 161.9f, 393.1f) curveTo(179.9f, 403.8f, 199.6f, 411.4f, 220.1f, 415.6f) curveTo(225.9f, 416.8f, 231.8f, 417.7f, 237.7f, 418.3f) curveTo(237.5f, 418.3f, 237.9f, 413.4f, 237.7f, 413.3f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(143.6f, 375.1f) curveTo(124.2f, 362.6f, 108.3f, 345.0f, 97.9f, 324.5f) curveTo(94.9f, 318.7f, 92.4f, 312.6f, 90.4f, 306.4f) curveTo(90.6f, 307.0f, 90.4f, 308.2f, 90.4f, 308.9f) curveTo(90.4f, 309.6f, 90.2f, 310.7f, 90.4f, 311.4f) curveTo(97.6f, 333.3f, 110.8f, 353.1f, 128.0f, 368.3f) curveTo(132.9f, 372.6f, 138.1f, 376.6f, 143.6f, 380.1f) curveTo(143.4f, 380.0f, 143.6f, 377.9f, 143.6f, 377.6f) curveTo(143.6f, 377.3f, 143.8f, 375.3f, 143.6f, 375.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(86.9f, 289.1f) curveTo(85.7f, 277.2f, 85.3f, 265.3f, 85.9f, 253.4f) curveTo(86.4f, 241.6f, 87.7f, 229.9f, 89.7f, 218.3f) curveTo(90.8f, 211.7f, 92.2f, 205.1f, 93.8f, 198.6f) curveTo(94.0f, 197.9f, 93.8f, 196.9f, 93.8f, 196.1f) curveTo(93.8f, 195.3f, 93.6f, 194.3f, 93.8f, 193.6f) curveTo(87.5f, 219.4f, 85.0f, 245.8f, 85.6f, 272.3f) curveTo(85.8f, 279.6f, 86.2f, 286.9f, 86.9f, 294.1f) curveTo(86.7f, 292.4f, 87.0f, 290.7f, 86.9f, 289.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(102.9f, 178.3f) curveTo(109.0f, 160.2f, 119.9f, 143.7f, 134.1f, 131.0f) curveTo(138.1f, 127.4f, 142.4f, 124.1f, 146.9f, 121.1f) curveTo(147.1f, 121.0f, 146.9f, 118.9f, 146.9f, 118.6f) curveTo(146.9f, 118.3f, 146.7f, 116.2f, 146.9f, 116.1f) curveTo(130.9f, 126.7f, 117.9f, 141.4f, 109.2f, 158.4f) curveTo(106.7f, 163.2f, 104.7f, 168.2f, 102.9f, 173.3f) curveTo(102.7f, 174.0f, 102.9f, 175.1f, 102.9f, 175.8f) curveTo(102.9f, 176.5f, 103.1f, 177.6f, 102.9f, 178.3f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(178.1f, 113.6f) curveTo(202.7f, 107.5f, 228.3f, 105.4f, 253.5f, 107.6f) curveTo(260.6f, 108.2f, 267.7f, 109.2f, 274.7f, 110.5f) curveTo(274.4f, 110.5f, 275.0f, 105.6f, 274.7f, 105.5f) curveTo(249.8f, 101.0f, 224.1f, 100.6f, 199.0f, 104.4f) curveTo(192.0f, 105.5f, 185.0f, 106.9f, 178.0f, 108.6f) curveTo(177.7f, 108.7f, 178.5f, 113.5f, 178.1f, 113.6f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(300.5f, 118.3f) curveTo(310.7f, 122.8f, 320.9f, 127.4f, 331.1f, 131.9f) curveTo(335.7f, 134.0f, 340.0f, 136.2f, 343.6f, 139.8f) curveTo(347.4f, 143.5f, 350.7f, 147.8f, 354.2f, 151.8f) curveTo(358.2f, 156.6f, 362.1f, 161.4f, 366.0f, 166.2f) curveTo(366.0f, 166.2f, 366.0f, 165.4f, 366.0f, 165.5f) curveTo(366.0f, 164.9f, 366.0f, 164.3f, 366.0f, 163.7f) curveTo(366.0f, 163.1f, 366.0f, 162.5f, 366.0f, 161.9f) curveTo(366.0f, 161.8f, 366.1f, 161.3f, 366.0f, 161.2f) curveTo(359.3f, 152.8f, 352.5f, 144.2f, 345.2f, 136.3f) curveTo(341.7f, 132.6f, 337.8f, 129.9f, 333.2f, 127.7f) curveTo(328.2f, 125.4f, 323.0f, 123.2f, 318.0f, 120.9f) curveTo(312.2f, 118.3f, 306.4f, 115.7f, 300.6f, 113.2f) curveTo(300.7f, 113.3f, 300.6f, 115.5f, 300.6f, 115.7f) curveTo(300.5f, 116.1f, 300.3f, 118.2f, 300.5f, 118.3f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(378.0f, 183.7f) curveTo(387.6f, 200.9f, 397.3f, 218.6f, 400.1f, 238.3f) curveTo(400.3f, 239.7f, 400.5f, 241.1f, 400.6f, 242.5f) curveTo(400.7f, 243.2f, 400.7f, 244.0f, 400.8f, 244.7f) curveTo(400.8f, 245.1f, 401.1f, 246.7f, 400.9f, 247.0f) curveTo(400.9f, 247.0f, 401.0f, 244.9f, 400.9f, 246.0f) curveTo(400.9f, 246.4f, 400.9f, 246.8f, 400.9f, 247.3f) curveTo(400.9f, 248.1f, 400.8f, 248.9f, 400.7f, 249.8f) curveTo(400.6f, 251.3f, 400.4f, 252.7f, 400.2f, 254.2f) curveTo(399.9f, 255.8f, 400.5f, 257.6f, 400.2f, 259.2f) curveTo(401.0f, 253.7f, 401.1f, 248.2f, 401.0f, 242.7f) curveTo(400.9f, 237.2f, 400.1f, 231.8f, 398.9f, 226.4f) curveTo(396.5f, 215.5f, 392.1f, 205.2f, 387.0f, 195.3f) curveTo(384.1f, 189.7f, 381.1f, 184.2f, 378.0f, 178.7f) curveTo(378.0f, 178.8f, 378.0f, 179.4f, 378.0f, 179.4f) curveTo(378.0f, 180.0f, 378.0f, 180.6f, 378.0f, 181.2f) curveTo(378.0f, 181.8f, 378.0f, 182.4f, 378.0f, 183.0f) curveTo(378.0f, 183.1f, 377.9f, 183.5f, 378.0f, 183.7f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(402.3f, 281.4f) curveTo(400.2f, 299.9f, 393.2f, 317.8f, 382.2f, 332.8f) curveTo(379.1f, 337.0f, 375.7f, 341.0f, 372.0f, 344.8f) curveTo(371.7f, 345.1f, 372.0f, 346.9f, 372.0f, 347.3f) curveTo(372.0f, 347.9f, 372.0f, 348.5f, 372.0f, 349.1f) curveTo(372.0f, 349.2f, 372.1f, 349.8f, 372.0f, 349.8f) curveTo(385.1f, 336.5f, 394.6f, 319.9f, 399.4f, 301.9f) curveTo(400.8f, 296.8f, 401.7f, 291.7f, 402.3f, 286.4f) curveTo(402.5f, 284.7f, 402.1f, 283.0f, 402.3f, 281.4f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(358.0f, 360.1f) curveTo(344.4f, 371.5f, 328.6f, 380.2f, 311.6f, 385.5f) curveTo(306.8f, 387.0f, 301.9f, 388.2f, 297.0f, 389.2f) curveTo(296.7f, 389.3f, 297.3f, 394.1f, 297.0f, 394.2f) curveTo(314.4f, 390.8f, 331.1f, 383.9f, 345.9f, 374.1f) curveTo(350.1f, 371.3f, 354.1f, 368.3f, 357.9f, 365.1f) curveTo(358.2f, 364.9f, 357.9f, 363.0f, 357.9f, 362.6f) curveTo(358.0f, 362.2f, 357.7f, 360.3f, 358.0f, 360.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(270.6f, 393.0f) curveTo(249.7f, 395.9f, 227.9f, 396.1f, 207.5f, 389.9f) curveTo(201.7f, 388.1f, 196.2f, 385.9f, 190.8f, 383.1f) curveTo(190.9f, 383.2f, 190.8f, 385.4f, 190.8f, 385.6f) curveTo(190.8f, 385.9f, 190.6f, 388.0f, 190.8f, 388.1f) curveTo(209.7f, 398.0f, 231.3f, 400.8f, 252.4f, 399.8f) curveTo(258.5f, 399.5f, 264.5f, 398.9f, 270.6f, 398.1f) curveTo(270.9f, 398.0f, 270.4f, 393.1f, 270.6f, 393.0f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(167.9f, 373.1f) curveTo(149.7f, 365.3f, 133.9f, 352.4f, 122.4f, 336.3f) curveTo(119.2f, 331.8f, 116.3f, 327.0f, 113.9f, 322.0f) curveTo(113.9f, 322.1f, 113.9f, 322.7f, 113.9f, 322.7f) curveTo(113.9f, 323.3f, 113.9f, 323.9f, 113.9f, 324.5f) curveTo(113.9f, 325.1f, 113.9f, 325.7f, 113.9f, 326.3f) curveTo(113.9f, 326.5f, 113.8f, 326.9f, 113.9f, 327.0f) curveTo(122.7f, 344.7f, 136.4f, 359.8f, 153.2f, 370.4f) curveTo(157.9f, 373.4f, 162.8f, 375.9f, 167.9f, 378.1f) curveTo(167.8f, 378.1f, 167.9f, 375.8f, 167.9f, 375.6f) curveTo(167.9f, 375.4f, 168.1f, 373.2f, 167.9f, 373.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(106.0f, 293.8f) curveTo(104.1f, 280.3f, 103.3f, 266.8f, 104.2f, 253.2f) curveTo(105.0f, 239.7f, 107.1f, 226.2f, 110.4f, 213.0f) curveTo(112.2f, 205.6f, 114.5f, 198.3f, 117.1f, 191.2f) curveTo(117.3f, 190.5f, 117.1f, 189.4f, 117.1f, 188.7f) curveTo(117.1f, 188.0f, 116.8f, 186.9f, 117.1f, 186.2f) curveTo(112.0f, 200.2f, 108.3f, 214.7f, 106.2f, 229.5f) curveTo(104.0f, 244.2f, 103.4f, 259.1f, 103.9f, 273.9f) curveTo(104.2f, 282.2f, 104.9f, 290.5f, 106.1f, 298.8f) curveTo(105.8f, 297.2f, 106.2f, 295.4f, 106.0f, 293.8f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(125.7f, 177.6f) curveTo(139.2f, 158.4f, 154.4f, 137.4f, 178.0f, 130.4f) curveTo(184.9f, 128.3f, 192.2f, 128.0f, 199.0f, 125.8f) curveTo(199.1f, 125.8f, 199.0f, 123.5f, 199.0f, 123.3f) curveTo(199.0f, 123.1f, 198.9f, 120.8f, 199.0f, 120.8f) curveTo(192.9f, 122.8f, 186.4f, 123.2f, 180.2f, 124.8f) curveTo(174.7f, 126.2f, 169.4f, 128.5f, 164.5f, 131.4f) curveTo(154.3f, 137.5f, 145.9f, 146.0f, 138.5f, 155.3f) curveTo(134.0f, 160.9f, 129.8f, 166.8f, 125.7f, 172.7f) curveTo(125.4f, 173.1f, 125.7f, 174.7f, 125.7f, 175.2f) curveTo(125.7f, 175.8f, 125.7f, 176.4f, 125.7f, 177.0f) curveTo(125.7f, 177.0f, 125.8f, 177.5f, 125.7f, 177.6f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(223.5f, 125.6f) curveTo(250.3f, 124.1f, 278.5f, 124.8f, 302.7f, 137.9f) curveTo(308.8f, 141.2f, 314.4f, 145.2f, 319.4f, 150.0f) curveTo(319.2f, 149.8f, 319.4f, 147.9f, 319.4f, 147.5f) curveTo(319.4f, 146.9f, 319.4f, 146.3f, 319.4f, 145.7f) curveTo(319.4f, 145.6f, 319.5f, 145.0f, 319.4f, 145.0f) curveTo(300.3f, 126.8f, 273.4f, 120.8f, 247.8f, 120.1f) curveTo(239.7f, 119.9f, 231.6f, 120.2f, 223.5f, 120.6f) curveTo(223.3f, 120.6f, 223.5f, 125.6f, 223.5f, 125.6f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(338.1f, 165.6f) curveTo(354.1f, 179.4f, 367.4f, 196.3f, 377.0f, 215.1f) curveTo(379.7f, 220.4f, 382.1f, 225.8f, 384.2f, 231.4f) curveTo(384.0f, 230.8f, 384.2f, 229.6f, 384.2f, 228.9f) curveTo(384.2f, 228.2f, 384.4f, 227.1f, 384.2f, 226.4f) curveTo(376.7f, 206.6f, 365.4f, 188.4f, 350.9f, 172.9f) curveTo(346.8f, 168.6f, 342.6f, 164.4f, 338.1f, 160.6f) curveTo(338.3f, 160.8f, 338.1f, 162.8f, 338.1f, 163.1f) curveTo(338.1f, 163.7f, 338.1f, 164.3f, 338.1f, 164.9f) curveTo(338.0f, 164.9f, 338.0f, 165.5f, 338.1f, 165.6f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(386.3f, 248.2f) curveTo(386.8f, 253.1f, 387.2f, 258.0f, 387.5f, 263.0f) curveTo(387.8f, 268.0f, 387.7f, 272.2f, 387.1f, 277.4f) curveTo(386.0f, 286.9f, 382.8f, 295.7f, 378.0f, 303.9f) curveTo(375.3f, 308.5f, 372.2f, 312.9f, 369.2f, 317.3f) curveTo(368.9f, 317.7f, 369.2f, 319.3f, 369.2f, 319.8f) curveTo(369.2f, 320.4f, 369.2f, 321.0f, 369.2f, 321.6f) curveTo(369.2f, 321.7f, 369.3f, 322.2f, 369.2f, 322.3f) curveTo(375.4f, 313.4f, 381.6f, 304.4f, 384.8f, 293.9f) curveTo(387.9f, 283.5f, 388.1f, 272.4f, 387.7f, 261.6f) curveTo(387.5f, 255.5f, 387.0f, 249.3f, 386.4f, 243.2f) curveTo(386.4f, 244.9f, 386.1f, 246.6f, 386.3f, 248.2f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(359.8f, 333.6f) curveTo(354.6f, 344.6f, 346.3f, 354.1f, 336.0f, 360.5f) curveTo(330.2f, 364.1f, 323.9f, 366.6f, 317.4f, 368.6f) curveTo(310.3f, 370.8f, 303.0f, 372.8f, 295.8f, 374.5f) curveTo(287.5f, 376.6f, 279.0f, 378.4f, 270.6f, 379.9f) curveTo(270.3f, 380.0f, 270.9f, 384.9f, 270.6f, 384.9f) curveTo(285.6f, 382.1f, 300.4f, 378.6f, 315.0f, 374.3f) curveTo(321.6f, 372.3f, 328.2f, 370.0f, 334.2f, 366.5f) curveTo(339.4f, 363.5f, 344.2f, 359.7f, 348.3f, 355.4f) curveTo(353.0f, 350.4f, 356.9f, 344.7f, 359.9f, 338.5f) curveTo(360.2f, 337.9f, 359.9f, 336.6f, 359.9f, 336.0f) curveTo(359.9f, 335.4f, 359.9f, 334.8f, 359.9f, 334.2f) curveTo(359.8f, 334.1f, 359.7f, 333.7f, 359.8f, 333.6f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(238.2f, 379.1f) curveTo(212.6f, 380.1f, 185.6f, 376.7f, 164.6f, 360.8f) curveTo(159.3f, 356.8f, 154.6f, 352.1f, 150.6f, 346.8f) curveTo(150.6f, 346.9f, 150.6f, 347.6f, 150.6f, 347.5f) curveTo(150.6f, 348.1f, 150.6f, 348.7f, 150.6f, 349.3f) curveTo(150.6f, 349.9f, 150.6f, 350.5f, 150.6f, 351.1f) curveTo(150.6f, 351.3f, 150.5f, 351.7f, 150.6f, 351.8f) curveTo(165.5f, 372.1f, 190.4f, 381.4f, 214.8f, 383.7f) curveTo(222.6f, 384.4f, 230.4f, 384.5f, 238.2f, 384.2f) curveTo(238.3f, 384.1f, 238.2f, 379.1f, 238.2f, 379.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(137.5f, 325.6f) curveTo(131.1f, 317.1f, 126.1f, 307.6f, 122.7f, 297.5f) curveTo(121.0f, 292.4f, 119.7f, 287.1f, 118.9f, 281.8f) curveTo(118.5f, 279.2f, 118.2f, 276.6f, 118.0f, 274.0f) curveTo(117.9f, 272.6f, 117.8f, 271.2f, 117.8f, 269.9f) curveTo(117.8f, 269.6f, 117.8f, 269.4f, 117.8f, 269.1f) curveTo(117.8f, 268.9f, 117.8f, 268.7f, 117.8f, 268.5f) curveTo(117.8f, 267.7f, 117.8f, 268.0f, 117.8f, 269.2f) curveTo(117.9f, 268.5f, 117.8f, 267.7f, 117.9f, 267.0f) curveTo(118.1f, 260.8f, 119.0f, 254.7f, 120.4f, 248.7f) curveTo(120.6f, 248.0f, 120.4f, 247.0f, 120.4f, 246.2f) curveTo(120.4f, 245.4f, 120.2f, 244.4f, 120.4f, 243.7f) curveTo(117.6f, 255.2f, 117.1f, 267.4f, 118.1f, 279.1f) curveTo(119.1f, 290.9f, 122.2f, 302.5f, 127.2f, 313.2f) curveTo(130.1f, 319.3f, 133.6f, 325.1f, 137.6f, 330.4f) curveTo(137.6f, 330.3f, 137.6f, 329.6f, 137.6f, 329.7f) curveTo(137.6f, 329.1f, 137.6f, 328.5f, 137.6f, 327.9f) curveTo(137.6f, 327.3f, 137.6f, 326.7f, 137.6f, 326.1f) curveTo(137.6f, 326.2f, 137.6f, 325.7f, 137.5f, 325.6f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(125.4f, 223.7f) curveTo(127.2f, 212.3f, 131.5f, 201.5f, 137.0f, 191.4f) curveTo(139.7f, 186.3f, 142.7f, 181.4f, 145.8f, 176.5f) curveTo(148.7f, 172.0f, 151.6f, 167.4f, 155.0f, 163.2f) curveTo(158.6f, 158.8f, 162.9f, 154.9f, 168.2f, 152.9f) curveTo(168.4f, 152.8f, 168.2f, 150.6f, 168.2f, 150.4f) curveTo(168.2f, 150.2f, 168.0f, 147.9f, 168.2f, 147.9f) curveTo(158.3f, 151.6f, 152.4f, 161.2f, 147.0f, 169.7f) curveTo(140.7f, 179.4f, 134.8f, 189.4f, 130.5f, 200.2f) curveTo(128.1f, 206.2f, 126.4f, 212.4f, 125.4f, 218.7f) curveTo(125.2f, 220.4f, 125.7f, 222.1f, 125.4f, 223.7f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(193.1f, 145.5f) curveTo(213.9f, 138.3f, 236.5f, 136.6f, 258.2f, 140.6f) curveTo(264.3f, 141.7f, 270.4f, 143.3f, 276.3f, 145.4f) curveTo(276.2f, 145.4f, 276.3f, 143.1f, 276.3f, 142.9f) curveTo(276.3f, 142.7f, 276.5f, 140.5f, 276.3f, 140.4f) curveTo(255.5f, 133.2f, 232.8f, 131.6f, 211.2f, 135.7f) curveTo(205.1f, 136.9f, 199.0f, 138.5f, 193.2f, 140.5f) curveTo(193.1f, 140.6f, 193.2f, 142.8f, 193.2f, 143.0f) curveTo(193.0f, 143.2f, 193.2f, 145.4f, 193.1f, 145.5f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(295.5f, 151.7f) curveTo(310.9f, 159.8f, 325.2f, 169.9f, 338.0f, 181.7f) curveTo(341.6f, 185.0f, 345.1f, 188.5f, 348.5f, 192.1f) curveTo(348.3f, 191.8f, 348.5f, 190.0f, 348.5f, 189.6f) curveTo(348.5f, 189.0f, 348.5f, 188.4f, 348.5f, 187.8f) curveTo(348.5f, 187.7f, 348.6f, 187.2f, 348.5f, 187.1f) curveTo(336.6f, 174.4f, 323.1f, 163.3f, 308.3f, 154.1f) curveTo(304.1f, 151.5f, 299.8f, 149.0f, 295.5f, 146.7f) curveTo(295.6f, 146.8f, 295.5f, 149.0f, 295.5f, 149.2f) curveTo(295.5f, 149.4f, 295.3f, 151.5f, 295.5f, 151.7f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(358.1f, 210.6f) curveTo(361.5f, 220.0f, 364.9f, 229.5f, 367.4f, 239.2f) curveTo(368.7f, 244.0f, 369.7f, 248.8f, 370.4f, 253.7f) curveTo(370.8f, 256.2f, 371.1f, 258.6f, 371.3f, 261.1f) curveTo(371.4f, 262.5f, 371.5f, 263.8f, 371.5f, 265.2f) curveTo(371.5f, 266.2f, 371.5f, 266.1f, 371.5f, 264.8f) curveTo(371.5f, 265.2f, 371.5f, 265.6f, 371.5f, 266.0f) curveTo(371.5f, 266.6f, 371.4f, 267.3f, 371.4f, 267.9f) curveTo(371.3f, 268.9f, 371.4f, 270.0f, 371.4f, 271.0f) curveTo(371.4f, 271.5f, 371.4f, 272.0f, 371.4f, 272.5f) curveTo(371.4f, 272.5f, 371.4f, 273.0f, 371.4f, 272.8f) curveTo(371.9f, 264.0f, 371.5f, 255.1f, 370.0f, 246.4f) curveTo(368.5f, 237.5f, 365.9f, 228.7f, 363.1f, 220.2f) curveTo(361.5f, 215.3f, 359.7f, 210.5f, 358.0f, 205.7f) curveTo(358.2f, 206.3f, 358.0f, 207.5f, 358.0f, 208.2f) curveTo(358.0f, 208.8f, 357.8f, 209.9f, 358.1f, 210.6f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(369.6f, 287.5f) curveTo(361.1f, 305.3f, 350.6f, 322.8f, 335.6f, 335.9f) curveTo(331.4f, 339.6f, 326.9f, 342.8f, 322.1f, 345.6f) curveTo(321.9f, 345.7f, 322.1f, 347.8f, 322.1f, 348.1f) curveTo(322.1f, 348.4f, 322.3f, 350.5f, 322.1f, 350.6f) curveTo(339.4f, 340.6f, 352.0f, 324.5f, 361.7f, 307.4f) curveTo(364.5f, 302.5f, 367.1f, 297.5f, 369.5f, 292.4f) curveTo(369.8f, 291.8f, 369.5f, 290.5f, 369.5f, 289.9f) curveTo(369.5f, 289.3f, 369.5f, 288.7f, 369.5f, 288.1f) curveTo(369.6f, 288.1f, 369.5f, 287.7f, 369.6f, 287.5f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(299.3f, 355.9f) curveTo(288.1f, 359.7f, 276.4f, 361.9f, 264.8f, 364.0f) curveTo(258.8f, 365.1f, 252.7f, 366.2f, 246.6f, 366.6f) curveTo(240.7f, 367.0f, 234.8f, 366.5f, 228.9f, 365.5f) curveTo(222.3f, 364.3f, 215.9f, 362.5f, 209.4f, 360.7f) curveTo(209.5f, 360.7f, 209.4f, 363.0f, 209.4f, 363.2f) curveTo(209.4f, 363.4f, 209.3f, 365.7f, 209.4f, 365.7f) curveTo(220.8f, 368.9f, 232.4f, 372.1f, 244.3f, 371.8f) curveTo(250.5f, 371.6f, 256.5f, 370.6f, 262.6f, 369.5f) curveTo(268.4f, 368.5f, 274.2f, 367.4f, 280.0f, 366.2f) curveTo(286.5f, 364.8f, 293.0f, 363.2f, 299.3f, 361.1f) curveTo(299.4f, 361.1f, 299.3f, 358.8f, 299.3f, 358.6f) curveTo(299.4f, 358.2f, 299.2f, 356.0f, 299.3f, 355.9f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(183.5f, 351.5f) curveTo(178.8f, 348.7f, 174.1f, 345.9f, 169.8f, 342.5f) curveTo(166.0f, 339.5f, 162.4f, 336.3f, 159.1f, 332.8f) curveTo(152.5f, 325.7f, 147.0f, 317.6f, 142.9f, 308.8f) curveTo(140.6f, 303.8f, 138.7f, 298.7f, 137.3f, 293.4f) curveTo(137.5f, 294.1f, 137.3f, 295.2f, 137.3f, 295.9f) curveTo(137.3f, 296.7f, 137.1f, 297.6f, 137.3f, 298.4f) curveTo(142.2f, 317.2f, 153.3f, 334.2f, 168.4f, 346.5f) curveTo(173.1f, 350.3f, 178.3f, 353.5f, 183.5f, 356.6f) curveTo(183.3f, 356.5f, 183.5f, 354.4f, 183.5f, 354.1f) curveTo(183.5f, 353.8f, 183.7f, 351.7f, 183.5f, 351.5f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(133.8f, 266.2f) curveTo(133.3f, 263.9f, 133.0f, 261.6f, 132.7f, 259.3f) curveTo(132.6f, 258.1f, 132.5f, 256.9f, 132.5f, 255.7f) curveTo(132.5f, 255.3f, 132.5f, 254.9f, 132.5f, 254.6f) curveTo(132.5f, 254.4f, 132.4f, 257.6f, 132.5f, 256.3f) curveTo(132.5f, 255.6f, 132.5f, 254.8f, 132.6f, 254.1f) curveTo(132.9f, 249.4f, 133.6f, 244.7f, 134.6f, 240.1f) curveTo(136.7f, 231.0f, 140.0f, 222.3f, 143.8f, 213.7f) curveTo(146.0f, 208.8f, 148.3f, 204.0f, 150.6f, 199.2f) curveTo(150.9f, 198.6f, 150.6f, 197.3f, 150.6f, 196.7f) curveTo(150.6f, 196.1f, 150.6f, 195.5f, 150.6f, 194.9f) curveTo(150.6f, 194.7f, 150.5f, 194.3f, 150.6f, 194.2f) curveTo(141.5f, 213.0f, 132.6f, 232.3f, 132.4f, 253.6f) curveTo(132.4f, 259.5f, 132.5f, 265.4f, 133.8f, 271.2f) curveTo(133.5f, 269.6f, 134.2f, 267.8f, 133.8f, 266.2f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(162.2f, 180.1f) curveTo(178.9f, 166.5f, 197.9f, 154.4f, 219.7f, 151.8f) curveTo(225.9f, 151.1f, 232.1f, 151.2f, 238.2f, 152.2f) curveTo(238.0f, 152.2f, 238.5f, 147.3f, 238.2f, 147.2f) curveTo(216.3f, 143.5f, 194.7f, 152.0f, 176.8f, 164.1f) curveTo(171.8f, 167.5f, 166.9f, 171.2f, 162.2f, 175.1f) curveTo(161.9f, 175.3f, 162.2f, 177.3f, 162.2f, 177.6f) curveTo(162.1f, 178.0f, 162.4f, 179.9f, 162.2f, 180.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(259.4f, 155.4f) curveTo(279.5f, 159.3f, 300.7f, 163.5f, 316.6f, 177.4f) curveTo(320.6f, 180.9f, 324.0f, 184.9f, 326.8f, 189.4f) curveTo(326.8f, 189.3f, 326.8f, 188.7f, 326.8f, 188.7f) curveTo(326.8f, 188.1f, 326.8f, 187.5f, 326.8f, 186.9f) curveTo(326.8f, 186.3f, 326.8f, 185.7f, 326.8f, 185.1f) curveTo(326.8f, 184.9f, 326.9f, 184.5f, 326.8f, 184.4f) curveTo(316.2f, 167.2f, 296.7f, 158.9f, 277.8f, 154.3f) curveTo(271.7f, 152.8f, 265.6f, 151.6f, 259.4f, 150.5f) curveTo(259.7f, 150.5f, 259.1f, 155.3f, 259.4f, 155.4f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(336.1f, 200.3f) curveTo(338.9f, 205.0f, 341.7f, 209.6f, 344.5f, 214.3f) curveTo(347.2f, 218.9f, 350.1f, 223.4f, 352.5f, 228.2f) curveTo(354.6f, 232.5f, 356.2f, 237.0f, 357.3f, 241.6f) curveTo(357.8f, 243.8f, 358.2f, 246.1f, 358.5f, 248.4f) curveTo(358.7f, 249.7f, 358.8f, 250.9f, 358.8f, 252.2f) curveTo(358.8f, 252.8f, 358.8f, 253.3f, 358.9f, 253.9f) curveTo(359.0f, 255.3f, 358.9f, 252.8f, 358.9f, 252.8f) curveTo(358.8f, 253.3f, 358.9f, 253.8f, 358.8f, 254.3f) curveTo(358.5f, 259.7f, 357.6f, 265.1f, 355.9f, 270.3f) curveTo(355.7f, 271.0f, 355.9f, 272.1f, 355.9f, 272.8f) curveTo(355.9f, 273.5f, 356.1f, 274.6f, 355.9f, 275.3f) curveTo(357.5f, 270.3f, 358.4f, 265.2f, 358.7f, 260.0f) curveTo(359.0f, 254.7f, 359.0f, 249.2f, 358.4f, 243.9f) curveTo(357.8f, 238.6f, 356.5f, 233.4f, 354.6f, 228.5f) curveTo(352.5f, 223.1f, 349.6f, 218.2f, 346.6f, 213.3f) curveTo(343.0f, 207.4f, 339.5f, 201.4f, 335.9f, 195.5f) curveTo(335.9f, 195.6f, 335.9f, 196.2f, 335.9f, 196.2f) curveTo(335.9f, 196.8f, 335.9f, 197.4f, 335.9f, 198.0f) curveTo(335.9f, 198.6f, 335.9f, 199.2f, 335.9f, 199.8f) curveTo(336.1f, 199.7f, 336.0f, 200.1f, 336.1f, 200.3f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(351.0f, 287.7f) curveTo(340.1f, 308.7f, 321.9f, 325.7f, 300.4f, 335.5f) curveTo(294.3f, 338.3f, 288.0f, 340.5f, 281.5f, 342.1f) curveTo(281.1f, 342.2f, 281.8f, 347.0f, 281.5f, 347.1f) curveTo(304.5f, 341.5f, 325.4f, 328.0f, 340.1f, 309.5f) curveTo(344.3f, 304.3f, 347.9f, 298.6f, 351.0f, 292.7f) curveTo(351.3f, 292.2f, 351.0f, 290.8f, 351.0f, 290.2f) curveTo(351.0f, 289.6f, 351.0f, 289.0f, 351.0f, 288.4f) curveTo(351.0f, 288.3f, 350.9f, 287.8f, 351.0f, 287.7f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(251.7f, 343.6f) curveTo(246.5f, 345.3f, 241.1f, 345.5f, 235.8f, 345.6f) curveTo(230.3f, 345.7f, 224.8f, 345.7f, 219.3f, 345.5f) curveTo(209.5f, 345.1f, 199.5f, 343.5f, 190.7f, 338.7f) curveTo(186.2f, 336.3f, 182.1f, 333.0f, 178.9f, 329.0f) curveTo(178.9f, 329.0f, 178.9f, 329.8f, 178.9f, 329.7f) curveTo(178.9f, 330.3f, 178.9f, 330.9f, 178.9f, 331.5f) curveTo(178.9f, 332.1f, 178.9f, 332.7f, 178.9f, 333.3f) curveTo(178.9f, 333.5f, 178.8f, 333.9f, 178.9f, 334.0f) curveTo(191.8f, 350.5f, 214.6f, 350.8f, 233.6f, 350.6f) curveTo(239.7f, 350.5f, 245.8f, 350.4f, 251.7f, 348.6f) curveTo(251.8f, 348.6f, 251.7f, 346.3f, 251.7f, 346.1f) curveTo(251.8f, 345.9f, 251.6f, 343.7f, 251.7f, 343.6f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(164.7f, 313.5f) curveTo(160.1f, 307.0f, 155.5f, 300.5f, 151.8f, 293.5f) curveTo(148.0f, 286.2f, 145.6f, 278.5f, 144.8f, 270.4f) curveTo(144.3f, 265.4f, 144.3f, 260.7f, 144.4f, 255.7f) curveTo(144.4f, 254.6f, 144.4f, 253.4f, 144.4f, 252.3f) curveTo(144.4f, 252.1f, 144.4f, 250.5f, 144.4f, 251.4f) curveTo(144.2f, 261.1f, 143.8f, 270.9f, 145.5f, 280.5f) curveTo(147.1f, 289.6f, 151.1f, 298.0f, 156.0f, 305.7f) curveTo(158.7f, 310.1f, 161.7f, 314.3f, 164.7f, 318.5f) curveTo(164.7f, 318.4f, 164.7f, 317.7f, 164.7f, 317.8f) curveTo(164.7f, 317.2f, 164.7f, 316.6f, 164.7f, 316.0f) curveTo(164.7f, 315.4f, 164.7f, 314.8f, 164.7f, 314.2f) curveTo(164.7f, 314.1f, 164.8f, 313.6f, 164.7f, 313.5f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(151.3f, 233.1f) curveTo(153.0f, 223.9f, 157.2f, 215.6f, 161.9f, 207.6f) curveTo(166.1f, 200.5f, 170.2f, 193.1f, 175.8f, 187.0f) curveTo(178.9f, 183.7f, 182.4f, 180.9f, 186.6f, 179.2f) curveTo(186.8f, 179.1f, 186.6f, 176.9f, 186.6f, 176.7f) curveTo(186.6f, 176.5f, 186.4f, 174.3f, 186.6f, 174.2f) curveTo(179.2f, 177.4f, 173.9f, 183.7f, 169.5f, 190.3f) curveTo(164.6f, 197.6f, 160.0f, 205.3f, 156.3f, 213.2f) curveTo(154.1f, 217.9f, 152.3f, 222.9f, 151.4f, 228.1f) curveTo(151.0f, 229.7f, 151.6f, 231.5f, 151.3f, 233.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(205.6f, 172.8f) curveTo(224.8f, 167.4f, 245.3f, 162.7f, 265.0f, 168.4f) curveTo(270.1f, 169.9f, 274.9f, 172.0f, 279.3f, 174.9f) curveTo(279.1f, 174.8f, 279.3f, 172.7f, 279.3f, 172.4f) curveTo(279.3f, 172.1f, 279.5f, 170.1f, 279.3f, 169.9f) curveTo(262.6f, 159.0f, 241.7f, 159.5f, 222.9f, 163.5f) curveTo(217.1f, 164.7f, 211.3f, 166.3f, 205.6f, 167.9f) curveTo(205.5f, 167.9f, 205.6f, 170.2f, 205.6f, 170.4f) curveTo(205.6f, 170.5f, 205.7f, 172.8f, 205.6f, 172.8f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(292.9f, 182.4f) curveTo(301.4f, 187.0f, 309.5f, 192.4f, 317.0f, 198.5f) curveTo(320.7f, 201.5f, 324.2f, 204.7f, 327.6f, 208.1f) curveTo(330.9f, 211.4f, 334.1f, 214.8f, 336.8f, 218.5f) curveTo(339.9f, 222.8f, 342.3f, 227.6f, 342.5f, 233.0f) curveTo(342.5f, 233.3f, 342.5f, 232.6f, 342.5f, 232.7f) curveTo(342.5f, 232.2f, 342.5f, 231.7f, 342.5f, 231.2f) curveTo(342.5f, 230.2f, 342.5f, 229.1f, 342.5f, 228.1f) curveTo(342.1f, 218.7f, 335.5f, 211.2f, 329.2f, 204.7f) curveTo(322.4f, 197.7f, 314.9f, 191.4f, 306.9f, 185.8f) curveTo(302.4f, 182.7f, 297.7f, 179.8f, 292.8f, 177.2f) curveTo(292.9f, 177.3f, 292.8f, 179.5f, 292.8f, 179.7f) curveTo(292.9f, 180.2f, 292.7f, 182.3f, 292.9f, 182.4f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(344.5f, 249.8f) curveTo(344.6f, 250.9f, 344.7f, 252.0f, 344.8f, 253.2f) curveTo(344.8f, 253.6f, 344.8f, 254.1f, 344.9f, 254.5f) curveTo(345.0f, 255.5f, 345.0f, 252.3f, 344.9f, 253.5f) curveTo(344.9f, 253.8f, 344.9f, 254.2f, 344.9f, 254.5f) curveTo(344.8f, 256.6f, 344.6f, 258.8f, 344.3f, 260.9f) curveTo(343.7f, 264.9f, 342.8f, 268.8f, 341.5f, 272.7f) curveTo(338.9f, 280.4f, 335.0f, 287.7f, 329.9f, 294.0f) curveTo(327.0f, 297.6f, 323.7f, 300.9f, 320.2f, 303.8f) curveTo(319.9f, 304.0f, 320.2f, 306.0f, 320.2f, 306.3f) curveTo(320.2f, 306.6f, 320.5f, 308.6f, 320.2f, 308.8f) curveTo(334.5f, 297.2f, 343.5f, 279.6f, 344.8f, 261.2f) curveTo(345.2f, 255.8f, 345.2f, 250.2f, 344.6f, 244.8f) curveTo(344.7f, 246.5f, 344.3f, 248.2f, 344.5f, 249.8f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(300.1f, 317.7f) curveTo(284.8f, 328.3f, 266.4f, 334.1f, 247.8f, 334.3f) curveTo(242.5f, 334.4f, 237.2f, 333.9f, 232.0f, 333.1f) curveTo(232.2f, 333.1f, 231.7f, 338.0f, 232.0f, 338.1f) curveTo(250.3f, 341.1f, 269.5f, 338.5f, 286.3f, 330.7f) curveTo(291.1f, 328.5f, 295.7f, 325.8f, 300.1f, 322.8f) curveTo(300.3f, 322.6f, 300.1f, 320.6f, 300.1f, 320.3f) curveTo(300.1f, 319.8f, 299.8f, 317.8f, 300.1f, 317.7f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(201.7f, 325.8f) curveTo(197.4f, 323.7f, 193.2f, 321.6f, 188.9f, 319.5f) curveTo(185.3f, 317.7f, 181.6f, 316.0f, 178.3f, 313.8f) curveTo(175.0f, 311.6f, 172.2f, 309.0f, 170.1f, 305.6f) curveTo(167.8f, 302.0f, 166.2f, 297.9f, 164.6f, 294.0f) curveTo(162.7f, 289.2f, 160.9f, 284.4f, 159.4f, 279.5f) curveTo(159.6f, 280.2f, 159.4f, 281.3f, 159.4f, 282.0f) curveTo(159.4f, 282.7f, 159.2f, 283.8f, 159.4f, 284.5f) curveTo(160.8f, 288.8f, 162.3f, 293.0f, 163.9f, 297.2f) curveTo(165.5f, 301.2f, 167.1f, 305.3f, 169.2f, 309.1f) curveTo(171.2f, 312.6f, 173.7f, 315.5f, 177.0f, 317.8f) curveTo(180.2f, 320.1f, 183.7f, 321.9f, 187.2f, 323.6f) curveTo(192.0f, 326.0f, 196.9f, 328.4f, 201.7f, 330.7f) curveTo(201.6f, 330.6f, 201.7f, 328.4f, 201.7f, 328.2f) curveTo(201.8f, 328.1f, 202.0f, 325.9f, 201.7f, 325.8f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(158.9f, 263.4f) curveTo(161.4f, 246.3f, 166.6f, 229.7f, 174.1f, 214.2f) curveTo(174.4f, 213.6f, 174.1f, 212.3f, 174.1f, 211.7f) curveTo(174.1f, 211.1f, 174.1f, 210.5f, 174.1f, 209.9f) curveTo(174.1f, 209.7f, 174.0f, 209.3f, 174.1f, 209.2f) curveTo(166.5f, 224.7f, 161.4f, 241.3f, 158.9f, 258.4f) curveTo(158.6f, 260.0f, 159.1f, 261.7f, 158.9f, 263.4f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(183.9f, 205.0f) curveTo(188.1f, 197.2f, 196.2f, 192.7f, 204.0f, 189.0f) curveTo(212.9f, 184.8f, 222.2f, 181.6f, 231.9f, 180.0f) curveTo(237.4f, 179.1f, 243.0f, 178.7f, 248.5f, 178.9f) curveTo(248.5f, 178.9f, 248.6f, 173.9f, 248.5f, 173.9f) curveTo(238.7f, 173.6f, 228.9f, 175.1f, 219.6f, 177.9f) curveTo(214.9f, 179.3f, 210.3f, 181.1f, 205.8f, 183.1f) curveTo(201.7f, 185.0f, 197.6f, 187.0f, 193.8f, 189.5f) curveTo(189.8f, 192.2f, 186.2f, 195.6f, 183.8f, 199.9f) curveTo(183.5f, 200.4f, 183.8f, 201.8f, 183.8f, 202.4f) curveTo(183.8f, 203.0f, 183.8f, 203.6f, 183.8f, 204.2f) curveTo(183.9f, 204.4f, 184.0f, 204.8f, 183.9f, 205.0f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(261.6f, 181.5f) curveTo(269.5f, 182.9f, 277.5f, 184.7f, 284.7f, 188.3f) curveTo(291.6f, 191.8f, 297.4f, 196.9f, 302.7f, 202.5f) curveTo(305.7f, 205.7f, 308.6f, 209.1f, 311.4f, 212.5f) curveTo(311.4f, 212.5f, 311.4f, 211.7f, 311.4f, 211.8f) curveTo(311.4f, 211.2f, 311.4f, 210.6f, 311.4f, 210.0f) curveTo(311.4f, 209.4f, 311.4f, 208.8f, 311.4f, 208.2f) curveTo(311.4f, 208.1f, 311.5f, 207.6f, 311.4f, 207.5f) curveTo(306.5f, 201.6f, 301.5f, 195.7f, 295.7f, 190.7f) curveTo(289.7f, 185.5f, 282.8f, 181.9f, 275.2f, 179.6f) curveTo(270.8f, 178.3f, 266.2f, 177.3f, 261.6f, 176.5f) curveTo(261.8f, 176.5f, 261.3f, 181.4f, 261.6f, 181.5f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(320.5f, 223.7f) curveTo(324.8f, 230.4f, 327.8f, 238.0f, 329.3f, 245.9f) curveTo(329.6f, 247.8f, 329.9f, 249.7f, 330.1f, 251.6f) curveTo(330.2f, 252.7f, 330.3f, 253.9f, 330.3f, 255.0f) curveTo(330.4f, 257.4f, 330.4f, 252.9f, 330.3f, 254.5f) curveTo(330.3f, 255.1f, 330.3f, 255.6f, 330.2f, 256.2f) curveTo(329.9f, 260.3f, 329.3f, 264.4f, 328.2f, 268.3f) curveTo(328.0f, 269.0f, 328.2f, 270.0f, 328.2f, 270.8f) curveTo(328.2f, 271.5f, 328.4f, 272.6f, 328.2f, 273.3f) curveTo(330.2f, 266.0f, 330.5f, 258.5f, 330.4f, 250.9f) curveTo(330.2f, 243.7f, 328.9f, 236.6f, 326.3f, 229.9f) curveTo(324.8f, 226.0f, 322.9f, 222.2f, 320.6f, 218.7f) curveTo(320.6f, 218.8f, 320.6f, 219.4f, 320.6f, 219.4f) curveTo(320.6f, 220.0f, 320.6f, 220.6f, 320.6f, 221.2f) curveTo(320.6f, 221.8f, 320.6f, 222.4f, 320.6f, 223.0f) curveTo(320.5f, 223.2f, 320.4f, 223.6f, 320.5f, 223.7f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(315.4f, 287.1f) curveTo(312.9f, 290.3f, 310.3f, 293.4f, 307.6f, 296.5f) curveTo(305.1f, 299.3f, 302.6f, 302.1f, 299.8f, 304.5f) curveTo(294.3f, 309.2f, 287.5f, 311.5f, 280.5f, 312.8f) curveTo(276.5f, 313.5f, 272.4f, 313.9f, 268.3f, 314.3f) curveTo(268.1f, 314.3f, 268.4f, 319.3f, 268.3f, 319.3f) curveTo(275.3f, 318.7f, 282.5f, 318.0f, 289.2f, 315.6f) curveTo(292.6f, 314.4f, 295.8f, 312.7f, 298.6f, 310.5f) curveTo(301.5f, 308.2f, 304.0f, 305.5f, 306.5f, 302.8f) curveTo(309.6f, 299.4f, 312.5f, 295.8f, 315.4f, 292.2f) curveTo(315.7f, 291.8f, 315.4f, 290.2f, 315.4f, 289.7f) curveTo(315.4f, 289.1f, 315.4f, 288.5f, 315.4f, 287.9f) curveTo(315.4f, 287.7f, 315.3f, 287.2f, 315.4f, 287.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(244.4f, 318.0f) curveTo(228.4f, 318.8f, 211.5f, 319.3f, 196.7f, 312.0f) curveTo(192.9f, 310.1f, 189.4f, 307.8f, 186.3f, 305.0f) curveTo(186.5f, 305.2f, 186.3f, 307.2f, 186.3f, 307.5f) curveTo(186.3f, 308.1f, 186.3f, 308.7f, 186.3f, 309.3f) curveTo(186.3f, 309.4f, 186.2f, 310.0f, 186.3f, 310.0f) curveTo(198.0f, 320.7f, 214.2f, 323.4f, 229.5f, 323.4f) curveTo(234.4f, 323.4f, 239.4f, 323.2f, 244.3f, 322.9f) curveTo(244.6f, 323.0f, 244.4f, 318.0f, 244.4f, 318.0f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(178.8f, 293.4f) curveTo(177.2f, 287.2f, 175.7f, 281.0f, 174.6f, 274.8f) curveTo(174.0f, 271.7f, 173.6f, 268.6f, 173.3f, 265.5f) curveTo(173.1f, 263.8f, 173.0f, 262.1f, 172.9f, 260.3f) curveTo(172.9f, 259.9f, 172.9f, 259.5f, 172.9f, 259.1f) curveTo(173.0f, 261.4f, 172.9f, 260.2f, 172.9f, 259.6f) curveTo(172.9f, 258.6f, 173.0f, 257.6f, 173.1f, 256.6f) curveTo(173.2f, 254.9f, 173.0f, 253.3f, 173.1f, 251.6f) curveTo(172.5f, 259.3f, 172.7f, 267.2f, 173.8f, 274.8f) curveTo(174.9f, 282.8f, 176.9f, 290.6f, 178.9f, 298.4f) curveTo(178.7f, 297.7f, 178.9f, 296.6f, 178.9f, 295.9f) curveTo(178.8f, 295.1f, 179.0f, 294.1f, 178.8f, 293.4f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(179.3f, 233.7f) curveTo(182.1f, 227.1f, 184.9f, 220.3f, 189.9f, 215.0f) curveTo(194.9f, 209.6f, 201.5f, 206.3f, 208.0f, 203.1f) curveTo(208.2f, 203.0f, 208.0f, 200.9f, 208.0f, 200.6f) curveTo(208.0f, 200.4f, 207.8f, 198.2f, 208.0f, 198.1f) curveTo(201.5f, 201.3f, 194.9f, 204.6f, 189.9f, 210.0f) curveTo(185.0f, 215.3f, 182.1f, 222.1f, 179.3f, 228.7f) curveTo(179.0f, 229.3f, 179.3f, 230.5f, 179.3f, 231.2f) curveTo(179.3f, 231.8f, 179.3f, 232.4f, 179.3f, 233.0f) curveTo(179.3f, 233.1f, 179.4f, 233.5f, 179.3f, 233.7f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(229.1f, 197.9f) curveTo(245.8f, 195.8f, 263.7f, 193.7f, 279.2f, 201.6f) curveTo(279.1f, 201.5f, 279.2f, 199.3f, 279.2f, 199.1f) curveTo(279.2f, 198.8f, 279.4f, 196.7f, 279.2f, 196.6f) curveTo(263.6f, 188.7f, 245.7f, 190.8f, 229.1f, 192.9f) curveTo(228.9f, 192.9f, 229.3f, 197.8f, 229.1f, 197.9f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(293.1f, 213.1f) curveTo(300.9f, 221.2f, 307.3f, 230.6f, 311.8f, 240.9f) curveTo(311.8f, 240.8f, 311.8f, 240.2f, 311.8f, 240.2f) curveTo(311.8f, 239.6f, 311.8f, 239.0f, 311.8f, 238.4f) curveTo(311.8f, 237.7f, 312.1f, 236.5f, 311.8f, 235.9f) curveTo(307.3f, 225.6f, 301.0f, 216.2f, 293.1f, 208.1f) curveTo(293.3f, 208.3f, 293.1f, 210.2f, 293.1f, 210.6f) curveTo(293.1f, 211.2f, 293.1f, 211.8f, 293.1f, 212.4f) curveTo(293.0f, 212.5f, 293.0f, 213.0f, 293.1f, 213.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(312.2f, 254.4f) curveTo(312.2f, 254.7f, 312.2f, 255.1f, 312.3f, 255.4f) curveTo(312.4f, 257.1f, 312.3f, 254.2f, 312.3f, 254.0f) curveTo(312.2f, 254.8f, 312.2f, 255.6f, 312.1f, 256.3f) curveTo(312.0f, 257.7f, 311.8f, 259.1f, 311.5f, 260.5f) curveTo(311.0f, 263.2f, 310.2f, 265.9f, 309.2f, 268.4f) curveTo(307.1f, 273.5f, 304.1f, 278.3f, 300.3f, 282.3f) curveTo(300.0f, 282.6f, 300.3f, 284.4f, 300.3f, 284.8f) curveTo(300.3f, 285.4f, 300.3f, 286.0f, 300.3f, 286.6f) curveTo(300.3f, 286.7f, 300.4f, 287.3f, 300.3f, 287.3f) curveTo(305.0f, 282.4f, 308.4f, 276.4f, 310.4f, 270.0f) curveTo(312.4f, 263.4f, 312.7f, 256.4f, 312.2f, 249.5f) curveTo(312.2f, 249.2f, 312.2f, 249.9f, 312.2f, 249.8f) curveTo(312.2f, 250.3f, 312.2f, 250.8f, 312.2f, 251.3f) curveTo(312.1f, 252.4f, 312.1f, 253.4f, 312.2f, 254.4f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(283.5f, 295.9f) curveTo(273.7f, 301.0f, 262.8f, 303.7f, 251.7f, 303.7f) verticalLineTo(308.7f) curveTo(262.8f, 308.7f, 273.7f, 306.0f, 283.5f, 300.9f) curveTo(283.7f, 300.8f, 283.5f, 298.7f, 283.5f, 298.4f) curveTo(283.5f, 298.2f, 283.3f, 296.0f, 283.5f, 295.9f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(220.5f, 303.4f) curveTo(210.0f, 301.3f, 198.4f, 298.7f, 191.1f, 290.2f) curveTo(191.1f, 290.2f, 191.1f, 291.0f, 191.1f, 290.9f) curveTo(191.1f, 291.5f, 191.1f, 292.1f, 191.1f, 292.7f) curveTo(191.1f, 293.3f, 191.1f, 293.9f, 191.1f, 294.5f) curveTo(191.1f, 294.6f, 191.0f, 295.1f, 191.1f, 295.2f) curveTo(198.4f, 303.8f, 209.9f, 306.3f, 220.5f, 308.4f) curveTo(220.2f, 308.3f, 220.9f, 303.5f, 220.5f, 303.4f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(185.6f, 271.5f) curveTo(185.6f, 270.5f, 185.5f, 269.4f, 185.5f, 268.4f) curveTo(185.5f, 267.3f, 185.3f, 266.1f, 185.4f, 265.1f) curveTo(185.2f, 266.9f, 185.4f, 264.7f, 185.4f, 264.3f) curveTo(185.4f, 263.9f, 185.4f, 263.4f, 185.5f, 263.0f) curveTo(185.6f, 261.8f, 185.7f, 260.5f, 185.8f, 259.3f) curveTo(186.3f, 255.1f, 187.3f, 251.0f, 189.0f, 247.2f) curveTo(189.3f, 246.6f, 189.0f, 245.3f, 189.0f, 244.7f) curveTo(189.0f, 244.1f, 189.0f, 243.5f, 189.0f, 242.9f) curveTo(189.0f, 242.7f, 188.9f, 242.3f, 189.0f, 242.2f) curveTo(186.7f, 247.4f, 185.7f, 253.0f, 185.4f, 258.6f) curveTo(185.1f, 264.5f, 185.3f, 270.5f, 185.5f, 276.4f) curveTo(185.5f, 276.7f, 185.5f, 276.0f, 185.5f, 276.1f) curveTo(185.5f, 275.6f, 185.5f, 275.1f, 185.5f, 274.6f) curveTo(185.6f, 273.6f, 185.6f, 272.5f, 185.6f, 271.5f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(200.1f, 233.1f) curveTo(207.3f, 225.4f, 216.3f, 219.5f, 226.2f, 215.9f) curveTo(226.4f, 215.8f, 226.2f, 213.6f, 226.2f, 213.4f) curveTo(226.2f, 213.2f, 226.0f, 210.9f, 226.2f, 210.9f) curveTo(216.3f, 214.5f, 207.3f, 220.4f, 200.1f, 228.1f) curveTo(199.8f, 228.4f, 200.1f, 230.2f, 200.1f, 230.6f) curveTo(200.1f, 231.2f, 200.1f, 231.8f, 200.1f, 232.4f) curveTo(200.0f, 232.5f, 200.1f, 233.1f, 200.1f, 233.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(250.1f, 215.6f) curveTo(260.4f, 215.5f, 270.5f, 218.7f, 278.8f, 224.7f) curveTo(278.6f, 224.6f, 278.8f, 222.5f, 278.8f, 222.2f) curveTo(278.8f, 221.9f, 279.1f, 219.9f, 278.8f, 219.7f) curveTo(270.4f, 213.7f, 260.3f, 210.5f, 250.1f, 210.6f) verticalLineTo(215.6f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(290.1f, 236.8f) curveTo(291.5f, 240.1f, 292.9f, 243.4f, 293.9f, 246.9f) curveTo(294.4f, 248.6f, 294.7f, 250.3f, 294.9f, 252.1f) curveTo(295.0f, 252.6f, 295.0f, 253.1f, 295.0f, 253.6f) curveTo(294.5f, 249.1f, 295.1f, 249.7f, 294.9f, 251.5f) curveTo(294.8f, 252.4f, 294.6f, 253.4f, 294.4f, 254.3f) curveTo(294.2f, 255.1f, 294.4f, 256.0f, 294.4f, 256.8f) curveTo(294.4f, 257.6f, 294.6f, 258.6f, 294.4f, 259.3f) curveTo(294.9f, 257.1f, 295.0f, 254.9f, 295.0f, 252.7f) curveTo(295.0f, 250.2f, 295.0f, 247.8f, 294.6f, 245.3f) curveTo(293.8f, 240.6f, 291.9f, 236.2f, 290.0f, 231.9f) curveTo(290.0f, 232.0f, 290.0f, 232.6f, 290.0f, 232.6f) curveTo(290.0f, 233.2f, 290.0f, 233.8f, 290.0f, 234.4f) curveTo(290.0f, 235.0f, 290.0f, 235.6f, 290.0f, 236.2f) curveTo(290.1f, 236.3f, 290.0f, 236.6f, 290.1f, 236.8f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(283.5f, 269.8f) curveTo(277.7f, 279.4f, 267.5f, 286.0f, 256.4f, 287.5f) curveTo(256.1f, 287.5f, 256.6f, 292.5f, 256.4f, 292.5f) curveTo(267.5f, 291.0f, 277.7f, 284.4f, 283.5f, 274.8f) curveTo(283.8f, 274.3f, 283.5f, 272.9f, 283.5f, 272.3f) curveTo(283.5f, 271.7f, 283.5f, 271.1f, 283.5f, 270.5f) curveTo(283.5f, 270.4f, 283.4f, 269.9f, 283.5f, 269.8f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(240.2f, 290.5f) curveTo(224.9f, 292.5f, 210.3f, 285.7f, 196.9f, 279.2f) curveTo(197.0f, 279.3f, 196.9f, 281.5f, 196.9f, 281.7f) curveTo(196.9f, 282.0f, 196.7f, 284.1f, 196.9f, 284.2f) curveTo(210.3f, 290.7f, 224.9f, 297.5f, 240.2f, 295.5f) curveTo(240.5f, 295.4f, 240.0f, 290.5f, 240.2f, 290.5f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(194.8f, 269.6f) curveTo(196.3f, 258.3f, 202.7f, 248.0f, 212.1f, 241.5f) curveTo(212.3f, 241.3f, 212.1f, 239.3f, 212.1f, 239.0f) curveTo(212.1f, 238.7f, 211.8f, 236.7f, 212.1f, 236.5f) curveTo(202.7f, 242.9f, 196.3f, 253.3f, 194.8f, 264.6f) curveTo(194.6f, 266.3f, 195.0f, 268.0f, 194.8f, 269.6f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(235.0f, 233.7f) curveTo(244.6f, 231.6f, 255.6f, 229.4f, 264.3f, 235.4f) curveTo(264.1f, 235.3f, 264.3f, 233.2f, 264.3f, 232.9f) curveTo(264.3f, 232.6f, 264.6f, 230.6f, 264.3f, 230.4f) curveTo(255.6f, 224.3f, 244.6f, 226.5f, 235.0f, 228.7f) curveTo(234.6f, 228.8f, 235.3f, 233.6f, 235.0f, 233.7f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(271.9f, 244.1f) curveTo(273.0f, 246.7f, 274.0f, 249.4f, 274.7f, 252.2f) curveTo(275.0f, 253.6f, 275.2f, 255.0f, 275.4f, 256.5f) curveTo(274.9f, 252.3f, 275.5f, 252.5f, 275.3f, 254.2f) curveTo(275.3f, 254.5f, 275.2f, 254.8f, 275.2f, 255.2f) curveTo(275.1f, 256.0f, 274.9f, 256.7f, 274.6f, 257.4f) curveTo(274.5f, 257.6f, 274.6f, 258.0f, 274.6f, 258.1f) curveTo(274.6f, 258.7f, 274.6f, 259.3f, 274.6f, 259.9f) curveTo(274.6f, 260.5f, 274.6f, 261.1f, 274.6f, 261.7f) curveTo(274.6f, 261.9f, 274.7f, 262.2f, 274.6f, 262.4f) curveTo(275.1f, 260.7f, 275.4f, 259.0f, 275.4f, 257.3f) curveTo(275.4f, 254.9f, 275.5f, 252.5f, 275.2f, 250.2f) curveTo(274.7f, 246.4f, 273.3f, 242.7f, 271.9f, 239.2f) curveTo(271.9f, 239.3f, 271.9f, 239.9f, 271.9f, 239.9f) curveTo(271.9f, 240.5f, 271.9f, 241.1f, 271.9f, 241.7f) curveTo(271.9f, 242.3f, 271.9f, 242.9f, 271.9f, 243.5f) curveTo(271.8f, 243.5f, 271.8f, 243.9f, 271.9f, 244.1f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(263.7f, 267.9f) curveTo(256.2f, 272.1f, 247.7f, 274.0f, 239.1f, 273.5f) curveTo(239.2f, 273.5f, 238.9f, 278.5f, 239.1f, 278.5f) curveTo(247.6f, 279.1f, 256.2f, 277.2f, 263.7f, 272.9f) curveTo(263.9f, 272.8f, 263.7f, 270.7f, 263.7f, 270.4f) curveTo(263.8f, 270.1f, 263.5f, 268.0f, 263.7f, 267.9f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(220.2f, 269.5f) curveTo(216.4f, 266.2f, 213.6f, 262.0f, 212.0f, 257.3f) curveTo(212.0f, 257.4f, 212.0f, 257.9f, 212.0f, 258.0f) curveTo(212.0f, 258.6f, 212.0f, 259.2f, 212.0f, 259.8f) curveTo(212.0f, 260.4f, 212.0f, 261.0f, 212.0f, 261.6f) curveTo(212.0f, 261.8f, 211.9f, 262.1f, 212.0f, 262.3f) curveTo(213.6f, 267.0f, 216.4f, 271.2f, 220.2f, 274.5f) curveTo(220.2f, 274.5f, 220.2f, 273.7f, 220.2f, 273.8f) curveTo(220.2f, 273.2f, 220.2f, 272.6f, 220.2f, 272.0f) curveTo(220.2f, 271.4f, 220.2f, 270.8f, 220.2f, 270.2f) curveTo(220.2f, 270.2f, 220.3f, 269.6f, 220.2f, 269.5f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(229.0f, 250.4f) curveTo(231.2f, 245.5f, 236.5f, 242.4f, 241.8f, 242.9f) curveTo(241.7f, 242.9f, 242.0f, 237.9f, 241.8f, 237.9f) curveTo(236.5f, 237.5f, 231.2f, 240.6f, 229.0f, 245.4f) curveTo(228.9f, 245.5f, 229.0f, 246.0f, 229.0f, 246.1f) curveTo(229.0f, 246.7f, 229.0f, 247.3f, 229.0f, 247.9f) curveTo(229.0f, 248.5f, 229.0f, 249.1f, 229.0f, 249.7f) curveTo(228.9f, 249.9f, 229.0f, 250.3f, 229.0f, 250.4f) close() } path(fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero) { moveTo(253.1f, 251.5f) curveTo(254.3f, 252.0f, 255.3f, 252.9f, 256.0f, 254.1f) curveTo(256.3f, 254.7f, 256.5f, 255.3f, 256.7f, 256.0f) curveTo(257.0f, 257.6f, 256.0f, 257.5f, 256.8f, 253.0f) curveTo(256.7f, 253.3f, 256.7f, 253.6f, 256.7f, 253.9f) curveTo(256.0f, 256.7f, 253.8f, 258.8f, 251.2f, 259.9f) curveTo(248.2f, 261.1f, 244.6f, 261.0f, 241.5f, 260.1f) curveTo(240.1f, 259.7f, 238.7f, 259.1f, 237.6f, 258.1f) curveTo(237.1f, 257.6f, 236.6f, 257.0f, 236.2f, 256.4f) curveTo(236.1f, 256.1f, 235.9f, 255.8f, 235.8f, 255.5f) curveTo(235.7f, 255.3f, 235.7f, 255.1f, 235.6f, 254.9f) curveTo(235.4f, 253.5f, 235.3f, 254.5f, 235.5f, 257.9f) curveTo(235.5f, 258.1f, 235.5f, 258.4f, 235.5f, 258.6f) curveTo(235.7f, 255.6f, 238.6f, 254.3f, 241.3f, 254.0f) curveTo(242.7f, 253.8f, 244.2f, 253.8f, 245.5f, 254.3f) curveTo(246.1f, 254.6f, 246.6f, 255.0f, 246.9f, 255.6f) curveTo(247.0f, 255.8f, 247.0f, 255.9f, 247.1f, 256.1f) curveTo(247.5f, 252.1f, 247.5f, 250.8f, 247.0f, 252.3f) curveTo(246.8f, 252.6f, 246.7f, 252.8f, 246.4f, 253.0f) curveTo(246.2f, 253.2f, 246.4f, 255.2f, 246.4f, 255.5f) curveTo(246.4f, 256.1f, 246.4f, 256.7f, 246.4f, 257.3f) curveTo(246.4f, 257.4f, 246.5f, 258.0f, 246.4f, 258.0f) curveTo(247.1f, 257.4f, 247.1f, 256.7f, 247.1f, 255.8f) curveTo(247.1f, 254.3f, 247.4f, 252.5f, 247.0f, 251.0f) curveTo(246.5f, 248.8f, 243.8f, 248.7f, 242.0f, 248.9f) curveTo(240.2f, 249.0f, 238.1f, 249.5f, 236.7f, 250.8f) curveTo(234.8f, 252.6f, 235.4f, 255.8f, 235.4f, 258.2f) curveTo(235.4f, 260.4f, 236.2f, 262.2f, 238.0f, 263.5f) curveTo(239.9f, 264.8f, 242.3f, 265.4f, 244.5f, 265.6f) curveTo(249.0f, 266.0f, 254.1f, 264.5f, 256.1f, 260.0f) curveTo(256.6f, 258.9f, 256.7f, 257.8f, 256.7f, 256.7f) curveTo(256.7f, 255.1f, 256.8f, 253.5f, 256.7f, 251.9f) curveTo(256.5f, 249.6f, 255.2f, 247.5f, 253.0f, 246.6f) curveTo(253.1f, 246.6f, 253.0f, 248.9f, 253.0f, 249.1f) curveTo(253.1f, 249.3f, 252.9f, 251.5f, 253.1f, 251.5f) close() } } } return _colony!! } private var _colony: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/ConcentricTriangles.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp val ConcentricTriangles: ImageVector get() { if (_concentricTriangles != null) { return _concentricTriangles!! } _concentricTriangles = icon( name = "ConcentricTriangles", viewPort = 107F to 144F, size = (85.6).dp to (115.2).dp, ) { path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(106.834f, 142.343f) lineTo(14.895f, 0.467f) curveTo(14.682f, 0.17f, 14.341f, 0.0f, 14.0f, 0.0f) curveTo(13.659f, 0.0f, 13.318f, 0.17f, 13.105f, 0.467f) lineTo(-78.835f, 142.343f) curveTo(-79.048f, 142.683f, -79.048f, 143.065f, -78.877f, 143.448f) curveTo(-78.707f, 143.788f, -78.323f, 144.0f, -77.94f, 144.0f) horizontalLineTo(105.94f) curveTo(106.323f, 144.0f, 106.707f, 143.788f, 106.877f, 143.448f) curveTo(107.047f, 143.065f, 107.047f, 142.64f, 106.834f, 142.343f) close() moveTo(-75.98f, 141.833f) lineTo(14.0f, 3.017f) lineTo(103.98f, 141.875f) horizontalLineTo(-75.98f) verticalLineTo(141.833f) close() } path( fill = SolidColor(Color(0xFF78786D)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(13.105f, 18.186f) lineTo(-60.685f, 132.06f) curveTo(-60.898f, 132.4f, -60.898f, 132.783f, -60.728f, 133.165f) curveTo(-60.557f, 133.505f, -60.174f, 133.717f, -59.79f, 133.717f) horizontalLineTo(87.833f) curveTo(88.216f, 133.717f, 88.6f, 133.505f, 88.77f, 133.165f) curveTo(88.941f, 132.825f, 88.941f, 132.4f, 88.728f, 132.06f) lineTo(14.937f, 18.186f) curveTo(14.724f, 17.889f, 14.384f, 17.719f, 14.043f, 17.719f) curveTo(13.702f, 17.719f, 13.318f, 17.889f, 13.105f, 18.186f) close() moveTo(85.831f, 131.593f) horizontalLineTo(-57.831f) lineTo(14.0f, 20.735f) lineTo(85.831f, 131.593f) close() } path( fill = SolidColor(Color(0xFF78786D)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(13.105f, 35.947f) lineTo(-42.578f, 121.82f) curveTo(-42.791f, 122.16f, -42.791f, 122.542f, -42.621f, 122.925f) curveTo(-42.451f, 123.265f, -42.067f, 123.477f, -41.684f, 123.477f) horizontalLineTo(69.684f) curveTo(70.067f, 123.477f, 70.451f, 123.265f, 70.621f, 122.925f) curveTo(70.791f, 122.585f, 70.791f, 122.16f, 70.578f, 121.82f) lineTo(14.895f, 35.947f) curveTo(14.682f, 35.649f, 14.341f, 35.479f, 14.0f, 35.479f) curveTo(13.659f, 35.479f, 13.318f, 35.649f, 13.105f, 35.947f) close() moveTo(67.724f, 121.353f) horizontalLineTo(-39.724f) lineTo(14.0f, 38.454f) lineTo(67.724f, 121.353f) close() } path( fill = SolidColor(Color(0xFF78786D)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(13.105f, 53.665f) lineTo(-24.429f, 111.58f) curveTo(-24.642f, 111.92f, -24.642f, 112.302f, -24.472f, 112.685f) curveTo(-24.301f, 113.024f, -23.918f, 113.237f, -23.534f, 113.237f) horizontalLineTo(51.534f) curveTo(51.918f, 113.237f, 52.301f, 113.024f, 52.472f, 112.685f) curveTo(52.642f, 112.345f, 52.642f, 111.92f, 52.429f, 111.58f) lineTo(14.895f, 53.665f) curveTo(14.682f, 53.368f, 14.341f, 53.198f, 14.0f, 53.198f) curveTo(13.659f, 53.198f, 13.318f, 53.368f, 13.105f, 53.665f) close() moveTo(49.575f, 111.112f) horizontalLineTo(-21.574f) lineTo(14.0f, 56.215f) lineTo(49.575f, 111.112f) close() } path( fill = SolidColor(Color(0xFF78786D)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(13.105f, 71.384f) lineTo(-6.28f, 101.34f) curveTo(-6.493f, 101.68f, -6.493f, 102.062f, -6.322f, 102.444f) curveTo(-6.152f, 102.784f, -5.768f, 102.997f, -5.385f, 102.997f) horizontalLineTo(33.428f) curveTo(33.811f, 102.997f, 34.194f, 102.784f, 34.365f, 102.444f) curveTo(34.535f, 102.104f, 34.535f, 101.68f, 34.322f, 101.34f) lineTo(14.937f, 71.384f) curveTo(14.724f, 71.087f, 14.383f, 70.916f, 14.043f, 70.916f) curveTo(13.702f, 70.916f, 13.318f, 71.087f, 13.105f, 71.384f) close() moveTo(31.468f, 100.872f) horizontalLineTo(-3.468f) lineTo(14.0f, 73.933f) lineTo(31.468f, 100.872f) close() } } return _concentricTriangles!! } private var _concentricTriangles: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/Dawn.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp val Dawn: ImageVector get() { if (_dawn != null) { return _dawn!! } _dawn = icon( name = "Dawn", viewPort = 92F to 92F, size = 164.dp to 164.dp, ) { path( fill = SolidColor(Color.Black), stroke = SolidColor(Color.Black), strokeLineWidth = 3.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(46.0f, 0.0f) lineTo(46.0089f, 45.9547f) lineTo(63.6035f, 3.5015f) lineTo(46.0256f, 45.9618f) lineTo(78.5268f, 13.4731f) lineTo(46.0382f, 45.9744f) lineTo(88.4984f, 28.3965f) lineTo(46.0453f, 45.9911f) lineTo(92.0f, 46.0f) lineTo(46.0453f, 46.0089f) lineTo(88.4984f, 63.6035f) lineTo(46.0382f, 46.0256f) lineTo(78.5268f, 78.5268f) lineTo(46.0256f, 46.0382f) lineTo(63.6035f, 88.4984f) lineTo(46.0089f, 46.0453f) lineTo(46.0f, 92.0f) lineTo(45.9911f, 46.0453f) lineTo(28.3965f, 88.4984f) lineTo(45.9744f, 46.0382f) lineTo(13.4731f, 78.5268f) lineTo(45.9618f, 46.0256f) lineTo(3.5015f, 63.6035f) lineTo(45.9547f, 46.0089f) lineTo(0.0f, 46.0f) lineTo(45.9547f, 45.9911f) lineTo(3.5015f, 28.3965f) lineTo(45.9618f, 45.9744f) lineTo(13.4731f, 13.4731f) lineTo(45.9744f, 45.9618f) lineTo(28.3965f, 3.5015f) lineTo(45.9911f, 45.9547f) lineTo(46.0f, 0.0f) close() } } return _dawn!! } private var _dawn: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/Helper.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp fun icon( name: String, viewPort: Pair, size: Pair = viewPort.first.dp to viewPort.second.dp, autoMirror: Boolean = false, block: ImageVector.Builder.() -> ImageVector.Builder, ): ImageVector { return ImageVector.Builder( name = name, defaultWidth = size.first, defaultHeight = size.second, viewportWidth = viewPort.first, viewportHeight = viewPort.second, autoMirror = autoMirror ).block().build() } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/QuarterCircles.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp val QuarterCircles: ImageVector get() { if (_quarterCircles != null) { return _quarterCircles!! } _quarterCircles = icon( name = "QuarterCircles", viewPort = 246F to 311F, size = 180.dp to 228.dp, ) { path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(247.924f, 192.73f) lineTo(247.136f, 63.733f) lineTo(118.115f, 65.16f) curveTo(118.348f, 99.402f, 131.87f, 130.272f, 153.731f, 153.174f) lineTo(25.528f, 154.57f) curveTo(26.027f, 225.81f, 84.129f, 282.901f, 155.424f, 282.141f) lineTo(154.533f, 154.004f) curveTo(178.177f, 178.225f, 211.308f, 193.116f, 247.924f, 192.73f) close() } } return _quarterCircles!! } private var _quarterCircles: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/Reveal.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp val Reveal: ImageVector get() { if (_reveal != null) { return _reveal!! } _reveal = icon( name = "Reveal", viewPort = 374F to 394F, size = 125.dp to 131.dp, ) { path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(187.0f, 187.6f) curveTo(290.277f, 187.6f, 374.0f, 145.783f, 374.0f, 94.2f) curveTo(374.0f, 42.616f, 290.277f, 0.8f, 187.0f, 0.8f) curveTo(83.723f, 0.8f, 0.0f, 42.616f, 0.0f, 94.2f) curveTo(0.0f, 145.783f, 83.723f, 187.6f, 187.0f, 187.6f) close() } path( fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(187.0f, 298.0f) curveTo(290.277f, 298.0f, 374.0f, 273.286f, 374.0f, 242.8f) curveTo(374.0f, 212.313f, 290.277f, 187.6f, 187.0f, 187.6f) curveTo(83.723f, 187.6f, 0.0f, 212.313f, 0.0f, 242.8f) curveTo(0.0f, 273.286f, 83.723f, 298.0f, 187.0f, 298.0f) close() } path( fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(187.0f, 361.4f) curveTo(290.277f, 361.4f, 374.0f, 347.207f, 374.0f, 329.7f) curveTo(374.0f, 312.193f, 290.277f, 298.0f, 187.0f, 298.0f) curveTo(83.723f, 298.0f, 0.0f, 312.193f, 0.0f, 329.7f) curveTo(0.0f, 347.207f, 83.723f, 361.4f, 187.0f, 361.4f) close() } path( fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(187.0f, 393.2f) curveTo(290.277f, 393.2f, 374.0f, 386.081f, 374.0f, 377.3f) curveTo(374.0f, 368.519f, 290.277f, 361.4f, 187.0f, 361.4f) curveTo(83.723f, 361.4f, 0.0f, 368.519f, 0.0f, 377.3f) curveTo(0.0f, 386.081f, 83.723f, 393.2f, 187.0f, 393.2f) close() } } return _reveal!! } private var _reveal: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/Stack.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path val Stack: ImageVector get() { if (_stack != null) { return _stack!! } _stack = icon( name = "Stack", viewPort = 65F to 100F, ) { path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(91.0f, 39.814f) curveTo(91.0f, 37.94f, 89.368f, 36.224f, 86.421f, 34.983f) lineTo(73.658f, 29.625f) lineTo(86.421f, 24.266f) curveTo(89.368f, 23.025f, 91.0f, 21.309f, 91.0f, 19.435f) curveTo(91.0f, 17.561f, 89.368f, 15.845f, 86.421f, 14.604f) lineTo(55.842f, 1.802f) curveTo(50.132f, -0.601f, 40.842f, -0.601f, 35.132f, 1.802f) lineTo(4.579f, 14.604f) curveTo(1.632f, 15.845f, 0.0f, 17.561f, 0.0f, 19.435f) curveTo(0.0f, 21.309f, 1.632f, 23.025f, 4.579f, 24.266f) lineTo(17.342f, 29.625f) lineTo(4.579f, 34.983f) curveTo(1.632f, 36.224f, 0.0f, 37.94f, 0.0f, 39.814f) curveTo(0.0f, 41.688f, 1.632f, 43.404f, 4.579f, 44.645f) lineTo(17.342f, 50.003f) lineTo(4.579f, 55.362f) curveTo(1.632f, 56.603f, 0.0f, 58.319f, 0.0f, 60.193f) curveTo(0.0f, 62.067f, 1.632f, 63.783f, 4.579f, 65.023f) lineTo(17.342f, 70.382f) lineTo(4.579f, 75.741f) curveTo(1.632f, 76.981f, 0.0f, 78.697f, 0.0f, 80.572f) curveTo(0.0f, 82.446f, 1.632f, 84.162f, 4.579f, 85.402f) lineTo(35.132f, 98.205f) curveTo(37.974f, 99.393f, 41.737f, 100.0f, 45.474f, 100.0f) curveTo(49.21f, 100.0f, 52.974f, 99.393f, 55.816f, 98.205f) lineTo(86.368f, 85.402f) curveTo(89.316f, 84.162f, 90.947f, 82.446f, 90.947f, 80.572f) curveTo(90.947f, 78.697f, 89.316f, 76.981f, 86.368f, 75.741f) lineTo(73.605f, 70.382f) lineTo(86.368f, 65.023f) curveTo(89.316f, 63.783f, 90.947f, 62.067f, 90.947f, 60.193f) curveTo(90.947f, 58.319f, 89.316f, 56.603f, 86.368f, 55.362f) lineTo(73.632f, 50.003f) lineTo(86.395f, 44.645f) curveTo(89.368f, 43.404f, 91.0f, 41.688f, 91.0f, 39.814f) close() moveTo(5.079f, 23.052f) curveTo(2.684f, 22.048f, 1.289f, 20.729f, 1.289f, 19.435f) curveTo(1.289f, 18.142f, 2.658f, 16.822f, 5.079f, 15.819f) lineTo(35.658f, 3.016f) curveTo(38.368f, 1.881f, 41.947f, 1.3f, 45.5f, 1.3f) curveTo(49.079f, 1.3f, 52.632f, 1.881f, 55.342f, 3.016f) lineTo(85.895f, 15.819f) curveTo(88.289f, 16.822f, 89.684f, 18.142f, 89.684f, 19.435f) curveTo(89.684f, 20.729f, 88.316f, 22.048f, 85.895f, 23.052f) lineTo(71.921f, 28.912f) lineTo(55.842f, 22.18f) curveTo(50.132f, 19.778f, 40.842f, 19.778f, 35.132f, 22.18f) lineTo(19.053f, 28.912f) lineTo(5.079f, 23.052f) close() moveTo(70.237f, 29.625f) lineTo(55.342f, 35.854f) curveTo(49.921f, 38.125f, 41.079f, 38.125f, 35.658f, 35.854f) lineTo(20.763f, 29.625f) lineTo(35.658f, 23.395f) curveTo(38.368f, 22.26f, 41.947f, 21.679f, 45.5f, 21.679f) curveTo(49.079f, 21.679f, 52.632f, 22.26f, 55.342f, 23.395f) lineTo(70.237f, 29.625f) close() moveTo(85.921f, 76.955f) curveTo(88.316f, 77.958f, 89.711f, 79.278f, 89.711f, 80.572f) curveTo(89.711f, 81.865f, 88.342f, 83.185f, 85.921f, 84.188f) lineTo(55.342f, 96.991f) curveTo(49.921f, 99.261f, 41.079f, 99.261f, 35.658f, 96.991f) lineTo(5.079f, 84.188f) curveTo(2.684f, 83.185f, 1.289f, 81.865f, 1.289f, 80.572f) curveTo(1.289f, 79.278f, 2.658f, 77.958f, 5.079f, 76.955f) lineTo(19.053f, 71.095f) lineTo(35.132f, 77.826f) curveTo(37.974f, 79.014f, 41.737f, 79.621f, 45.474f, 79.621f) curveTo(49.21f, 79.621f, 52.974f, 79.014f, 55.816f, 77.826f) lineTo(71.895f, 71.095f) lineTo(85.921f, 76.955f) close() moveTo(20.763f, 70.382f) lineTo(35.658f, 64.152f) curveTo(38.368f, 63.017f, 41.947f, 62.437f, 45.5f, 62.437f) curveTo(49.079f, 62.437f, 52.632f, 63.017f, 55.342f, 64.152f) lineTo(70.237f, 70.382f) lineTo(55.342f, 76.612f) curveTo(49.921f, 78.882f, 41.079f, 78.882f, 35.658f, 76.612f) lineTo(20.763f, 70.382f) close() moveTo(85.921f, 56.576f) curveTo(88.316f, 57.579f, 89.711f, 58.899f, 89.711f, 60.193f) curveTo(89.711f, 61.486f, 88.342f, 62.806f, 85.921f, 63.809f) lineTo(71.947f, 69.669f) lineTo(55.842f, 62.938f) curveTo(50.132f, 60.536f, 40.842f, 60.536f, 35.132f, 62.938f) lineTo(19.053f, 69.669f) lineTo(5.079f, 63.809f) curveTo(2.684f, 62.806f, 1.289f, 61.486f, 1.289f, 60.193f) curveTo(1.289f, 58.899f, 2.658f, 57.579f, 5.079f, 56.576f) lineTo(19.053f, 50.716f) lineTo(35.132f, 57.447f) curveTo(37.974f, 58.635f, 41.737f, 59.242f, 45.474f, 59.242f) curveTo(49.21f, 59.242f, 52.974f, 58.635f, 55.816f, 57.447f) lineTo(71.895f, 50.716f) lineTo(85.921f, 56.576f) close() moveTo(20.763f, 50.003f) lineTo(35.658f, 43.773f) curveTo(38.368f, 42.638f, 41.947f, 42.058f, 45.5f, 42.058f) curveTo(49.079f, 42.058f, 52.632f, 42.638f, 55.342f, 43.773f) lineTo(70.237f, 50.003f) lineTo(55.342f, 56.233f) curveTo(49.921f, 58.503f, 41.079f, 58.503f, 35.658f, 56.233f) lineTo(20.763f, 50.003f) close() moveTo(71.947f, 49.291f) lineTo(55.868f, 42.559f) curveTo(50.158f, 40.157f, 40.868f, 40.157f, 35.158f, 42.559f) lineTo(19.079f, 49.291f) lineTo(5.105f, 43.43f) curveTo(2.711f, 42.427f, 1.316f, 41.107f, 1.316f, 39.814f) curveTo(1.316f, 38.52f, 2.684f, 37.201f, 5.105f, 36.197f) lineTo(19.079f, 30.337f) lineTo(35.158f, 37.069f) curveTo(38.0f, 38.257f, 41.763f, 38.864f, 45.5f, 38.864f) curveTo(49.237f, 38.864f, 53.0f, 38.257f, 55.842f, 37.069f) lineTo(71.921f, 30.337f) lineTo(85.895f, 36.197f) curveTo(88.289f, 37.201f, 89.684f, 38.52f, 89.684f, 39.814f) curveTo(89.684f, 41.107f, 88.316f, 42.427f, 85.895f, 43.43f) lineTo(71.947f, 49.291f) close() } } return _stack!! } private var _stack: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/components/icons/Wireframe.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.components.icons import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path val Wireframe: ImageVector get() { if (wireframe != null) { return wireframe!! } wireframe = icon( name = "Wireframe", viewPort = 119.0f to 154.0f, ) { path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(101.271f, 153.154f) curveTo(99.535f, 153.154f, 97.763f, 152.98f, 95.922f, 152.632f) curveTo(89.358f, 151.378f, 82.828f, 148.138f, 76.507f, 145.038f) curveTo(75.708f, 144.655f, 74.909f, 144.237f, 74.11f, 143.854f) curveTo(73.068f, 143.331f, 72.026f, 142.809f, 70.949f, 142.286f) curveTo(64.906f, 139.256f, 58.619f, 136.121f, 52.054f, 134.797f) curveTo(44.239f, 133.23f, 36.181f, 135.146f, 28.366f, 136.992f) curveTo(27.394f, 137.236f, 26.456f, 137.445f, 25.483f, 137.654f) curveTo(25.24f, 137.688f, 24.997f, 137.723f, 24.719f, 137.758f) curveTo(21.871f, 138.106f, 17.946f, 138.559f, 16.244f, 135.494f) curveTo(13.188f, 127.099f, 13.188f, 118.565f, 13.223f, 110.309f) curveTo(13.223f, 104.457f, 13.223f, 98.396f, 12.181f, 92.405f) curveTo(10.687f, 84.01f, 7.631f, 75.511f, 4.644f, 67.29f) curveTo(3.15f, 63.145f, 1.587f, 58.861f, 0.302f, 54.716f) curveTo(-0.532f, 52.068f, 0.406f, 49.177f, 2.56f, 47.784f) curveTo(6.415f, 45.485f, 10.479f, 43.569f, 14.404f, 41.688f) curveTo(20.343f, 38.867f, 26.491f, 35.975f, 31.944f, 31.691f) curveTo(37.918f, 27.023f, 43.545f, 21.729f, 48.963f, 16.608f) curveTo(54.486f, 11.383f, 60.182f, 6.019f, 66.295f, 1.247f) curveTo(68.414f, -0.356f, 71.366f, -0.425f, 73.763f, 1.107f) curveTo(79.702f, 4.416f, 86.301f, 5.287f, 92.658f, 6.123f) curveTo(98.805f, 6.924f, 105.161f, 7.76f, 110.823f, 10.826f) curveTo(119.367f, 15.459f, 124.022f, 24.236f, 128.502f, 32.701f) curveTo(131.211f, 37.787f, 133.99f, 43.047f, 137.568f, 47.261f) curveTo(139.652f, 49.386f, 140.485f, 52.208f, 139.721f, 54.646f) curveTo(138.158f, 59.488f, 136.56f, 64.364f, 134.963f, 69.102f) curveTo(132.462f, 76.626f, 129.891f, 84.428f, 127.53f, 92.161f) curveTo(123.848f, 104.144f, 122.459f, 116.754f, 121.139f, 128.98f) curveTo(120.757f, 132.603f, 120.34f, 136.365f, 119.854f, 140.057f) curveTo(119.402f, 143.401f, 117.665f, 146.71f, 115.581f, 148.243f) curveTo(111.205f, 151.517f, 106.516f, 153.154f, 101.271f, 153.154f) close() moveTo(45.837f, 133.857f) curveTo(47.921f, 133.857f, 50.005f, 134.031f, 52.054f, 134.449f) curveTo(58.688f, 135.773f, 64.975f, 138.908f, 71.053f, 141.973f) curveTo(72.095f, 142.495f, 73.172f, 143.018f, 74.214f, 143.54f) curveTo(75.013f, 143.924f, 75.812f, 144.342f, 76.611f, 144.725f) curveTo(89.427f, 151.064f, 102.661f, 157.613f, 115.269f, 147.964f) curveTo(117.283f, 146.466f, 118.951f, 143.262f, 119.402f, 140.022f) curveTo(119.854f, 136.365f, 120.27f, 132.603f, 120.687f, 128.98f) curveTo(122.042f, 116.754f, 123.396f, 104.109f, 127.113f, 92.127f) curveTo(129.475f, 84.394f, 132.08f, 76.626f, 134.58f, 69.067f) curveTo(136.143f, 64.33f, 137.776f, 59.453f, 139.339f, 54.611f) curveTo(140.068f, 52.312f, 139.269f, 49.63f, 137.255f, 47.575f) curveTo(133.643f, 43.325f, 130.864f, 38.065f, 128.155f, 32.945f) curveTo(123.674f, 24.515f, 119.055f, 15.772f, 110.615f, 11.209f) curveTo(105.022f, 8.178f, 98.666f, 7.342f, 92.553f, 6.541f) curveTo(86.162f, 5.705f, 79.528f, 4.834f, 73.52f, 1.49f) curveTo(71.227f, 0.027f, 68.448f, 0.062f, 66.434f, 1.595f) curveTo(60.356f, 6.332f, 54.659f, 11.697f, 49.137f, 16.922f) curveTo(43.718f, 22.042f, 38.092f, 27.372f, 32.118f, 32.039f) curveTo(26.63f, 36.324f, 20.482f, 39.25f, 14.508f, 42.071f) curveTo(10.583f, 43.917f, 6.554f, 45.833f, 2.698f, 48.132f) curveTo(0.719f, 49.421f, -0.184f, 52.173f, 0.614f, 54.646f) curveTo(1.9f, 58.791f, 3.463f, 63.076f, 4.956f, 67.221f) curveTo(7.908f, 75.441f, 11.0f, 83.941f, 12.493f, 92.37f) curveTo(13.57f, 98.396f, 13.57f, 104.457f, 13.57f, 110.344f) curveTo(13.57f, 118.565f, 13.57f, 127.029f, 16.557f, 135.355f) curveTo(18.155f, 138.176f, 21.941f, 137.723f, 24.685f, 137.41f) curveTo(24.928f, 137.375f, 25.171f, 137.34f, 25.414f, 137.34f) curveTo(26.352f, 137.131f, 27.29f, 136.887f, 28.262f, 136.678f) curveTo(34.028f, 135.25f, 39.967f, 133.857f, 45.837f, 133.857f) close() } path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(100.924f, 146.85f) curveTo(92.831f, 146.85f, 84.634f, 142.983f, 77.166f, 139.43f) curveTo(76.055f, 138.908f, 74.978f, 138.385f, 73.901f, 137.898f) curveTo(73.172f, 137.549f, 72.443f, 137.236f, 71.713f, 136.887f) curveTo(65.982f, 134.205f, 60.078f, 131.453f, 53.895f, 129.886f) curveTo(46.219f, 127.9f, 38.995f, 129.329f, 31.388f, 130.826f) lineTo(30.902f, 130.931f) curveTo(26.769f, 131.802f, 23.26f, 130.687f, 22.462f, 128.214f) curveTo(20.621f, 121.735f, 20.1f, 115.012f, 19.579f, 108.498f) curveTo(19.093f, 102.472f, 18.606f, 96.202f, 17.078f, 90.211f) curveTo(15.411f, 83.801f, 13.084f, 77.392f, 10.826f, 71.227f) curveTo(9.089f, 66.524f, 7.318f, 61.647f, 5.859f, 56.806f) curveTo(5.06f, 54.054f, 5.929f, 51.337f, 7.943f, 50.362f) curveTo(10.791f, 49.003f, 13.605f, 47.819f, 16.349f, 46.704f) curveTo(22.601f, 44.126f, 28.471f, 41.688f, 34.097f, 37.229f) curveTo(40.419f, 32.248f, 46.184f, 26.396f, 51.777f, 20.719f) curveTo(56.396f, 16.016f, 61.189f, 11.174f, 66.26f, 6.855f) curveTo(68.344f, 5.183f, 71.297f, 4.8f, 74.006f, 5.88f) curveTo(79.077f, 7.482f, 84.599f, 7.482f, 89.948f, 7.482f) curveTo(96.582f, 7.482f, 103.425f, 7.447f, 109.329f, 10.547f) curveTo(118.568f, 15.389f, 123.084f, 25.7f, 127.495f, 35.662f) curveTo(129.197f, 39.528f, 130.795f, 43.151f, 132.635f, 46.391f) curveTo(134.476f, 49.351f, 135.136f, 52.347f, 134.407f, 54.611f) curveTo(132.74f, 59.906f, 130.933f, 65.235f, 129.232f, 70.425f) curveTo(127.078f, 76.87f, 124.855f, 83.558f, 122.806f, 90.141f) curveTo(118.951f, 102.542f, 118.013f, 115.709f, 117.075f, 128.458f) curveTo(116.901f, 130.722f, 116.728f, 133.021f, 116.554f, 135.32f) curveTo(116.241f, 139.117f, 114.539f, 142.53f, 112.386f, 143.784f) curveTo(108.635f, 145.979f, 104.779f, 146.85f, 100.924f, 146.85f) close() moveTo(45.907f, 128.527f) curveTo(48.581f, 128.527f, 51.256f, 128.806f, 53.999f, 129.503f) curveTo(60.182f, 131.105f, 66.121f, 133.857f, 71.852f, 136.539f) curveTo(72.582f, 136.887f, 73.311f, 137.236f, 74.04f, 137.549f) curveTo(75.117f, 138.037f, 76.194f, 138.559f, 77.305f, 139.082f) curveTo(88.281f, 144.272f, 100.75f, 150.194f, 112.178f, 143.436f) curveTo(114.262f, 142.252f, 115.859f, 138.942f, 116.172f, 135.25f) curveTo(116.346f, 132.986f, 116.519f, 130.652f, 116.693f, 128.388f) curveTo(117.631f, 115.639f, 118.568f, 102.437f, 122.424f, 90.002f) curveTo(124.473f, 83.383f, 126.696f, 76.73f, 128.849f, 70.286f) curveTo(130.586f, 65.096f, 132.358f, 59.766f, 134.025f, 54.472f) curveTo(134.719f, 52.347f, 134.059f, 49.351f, 132.288f, 46.53f) curveTo(130.447f, 43.256f, 128.849f, 39.633f, 127.148f, 35.766f) curveTo(122.806f, 25.839f, 118.291f, 15.598f, 109.156f, 10.826f) curveTo(103.321f, 7.761f, 96.513f, 7.795f, 89.914f, 7.795f) curveTo(84.53f, 7.795f, 78.973f, 7.83f, 73.832f, 6.193f) curveTo(71.227f, 5.148f, 68.414f, 5.496f, 66.434f, 7.099f) curveTo(61.363f, 11.418f, 56.604f, 16.26f, 51.985f, 20.962f) curveTo(46.393f, 26.64f, 40.627f, 32.492f, 34.271f, 37.473f) curveTo(28.609f, 41.897f, 22.705f, 44.37f, 16.453f, 46.948f) curveTo(13.709f, 48.097f, 10.896f, 49.247f, 8.047f, 50.605f) curveTo(6.172f, 51.511f, 5.373f, 54.054f, 6.137f, 56.632f) curveTo(7.596f, 61.473f, 9.402f, 66.315f, 11.104f, 71.018f) curveTo(13.362f, 77.218f, 15.723f, 83.592f, 17.391f, 90.037f) curveTo(18.954f, 96.063f, 19.44f, 102.333f, 19.891f, 108.359f) curveTo(20.412f, 114.838f, 20.933f, 121.561f, 22.74f, 128.005f) curveTo(23.504f, 130.304f, 26.838f, 131.349f, 30.798f, 130.513f) lineTo(31.284f, 130.408f) curveTo(36.251f, 129.468f, 41.044f, 128.527f, 45.907f, 128.527f) close() } path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(100.056f, 139.36f) curveTo(92.38f, 139.36f, 84.426f, 135.947f, 77.201f, 132.847f) curveTo(75.881f, 132.289f, 74.666f, 131.767f, 73.415f, 131.244f) curveTo(60.356f, 125.845f, 50.075f, 122.188f, 36.077f, 124.208f) curveTo(32.291f, 124.835f, 29.13f, 123.476f, 28.401f, 120.968f) curveTo(27.151f, 116.161f, 26.386f, 111.18f, 25.622f, 106.373f) curveTo(24.685f, 100.278f, 23.677f, 93.973f, 21.767f, 88.016f) curveTo(20.204f, 83.14f, 18.259f, 78.298f, 16.383f, 73.595f) curveTo(14.542f, 68.997f, 12.632f, 64.26f, 11.069f, 59.488f) curveTo(10.236f, 56.806f, 11.069f, 54.263f, 12.979f, 53.601f) lineTo(14.681f, 53.009f) curveTo(22.601f, 50.327f, 29.443f, 47.993f, 36.216f, 42.559f) curveTo(42.919f, 37.229f, 48.789f, 30.75f, 54.486f, 24.48f) curveTo(58.272f, 20.335f, 62.162f, 16.051f, 66.26f, 12.115f) curveTo(68.379f, 10.199f, 71.436f, 9.363f, 74.457f, 9.885f) curveTo(78.521f, 10.268f, 83.002f, 9.641f, 87.309f, 9.084f) curveTo(94.359f, 8.109f, 101.619f, 7.133f, 107.766f, 10.199f) curveTo(117.249f, 14.936f, 121.521f, 26.431f, 125.272f, 36.568f) curveTo(126.244f, 39.215f, 127.182f, 41.688f, 128.155f, 43.987f) curveTo(129.787f, 47.54f, 130.274f, 50.884f, 129.509f, 53.253f) curveTo(127.773f, 58.617f, 125.967f, 64.016f, 124.195f, 69.241f) curveTo(122.111f, 75.372f, 119.993f, 81.711f, 118.013f, 87.981f) curveTo(114.088f, 100.382f, 113.463f, 113.584f, 112.803f, 126.333f) curveTo(112.768f, 127.343f, 112.699f, 128.318f, 112.664f, 129.328f) curveTo(112.455f, 133.474f, 110.753f, 136.887f, 108.461f, 137.758f) curveTo(105.752f, 138.873f, 102.904f, 139.36f, 100.056f, 139.36f) close() moveTo(43.684f, 123.337f) curveTo(54.138f, 123.337f, 62.926f, 126.577f, 73.554f, 130.966f) curveTo(74.77f, 131.488f, 76.02f, 132.011f, 77.34f, 132.568f) curveTo(87.204f, 136.818f, 98.388f, 141.59f, 108.426f, 137.445f) curveTo(110.58f, 136.609f, 112.212f, 133.299f, 112.421f, 129.328f) curveTo(112.455f, 128.318f, 112.525f, 127.343f, 112.56f, 126.333f) curveTo(113.185f, 113.549f, 113.845f, 100.347f, 117.77f, 87.912f) curveTo(119.749f, 81.642f, 121.903f, 75.302f, 123.952f, 69.171f) curveTo(125.723f, 63.946f, 127.564f, 58.513f, 129.266f, 53.183f) curveTo(129.996f, 50.919f, 129.509f, 47.645f, 127.912f, 44.161f) curveTo(126.939f, 41.862f, 126.001f, 39.354f, 125.029f, 36.742f) curveTo(121.278f, 26.64f, 117.075f, 15.215f, 107.697f, 10.547f) curveTo(101.653f, 7.517f, 94.429f, 8.527f, 87.448f, 9.467f) curveTo(83.106f, 10.059f, 78.59f, 10.686f, 74.492f, 10.268f) curveTo(71.574f, 9.746f, 68.622f, 10.582f, 66.573f, 12.428f) curveTo(62.474f, 16.329f, 58.584f, 20.614f, 54.798f, 24.759f) curveTo(49.102f, 31.029f, 43.197f, 37.508f, 36.459f, 42.872f) curveTo(29.617f, 48.306f, 22.74f, 50.675f, 14.82f, 53.357f) lineTo(13.118f, 53.949f) curveTo(11.382f, 54.576f, 10.652f, 56.91f, 11.417f, 59.418f) curveTo(12.979f, 64.19f, 14.855f, 68.928f, 16.696f, 73.526f) curveTo(18.572f, 78.228f, 20.517f, 83.07f, 22.114f, 87.981f) curveTo(24.025f, 93.973f, 25.032f, 100.278f, 25.97f, 106.373f) curveTo(26.734f, 111.18f, 27.498f, 116.161f, 28.714f, 120.934f) curveTo(29.408f, 123.302f, 32.395f, 124.521f, 36.008f, 123.929f) curveTo(38.717f, 123.511f, 41.252f, 123.337f, 43.684f, 123.337f) close() } path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(98.458f, 130.931f) curveTo(91.233f, 130.931f, 83.662f, 128.144f, 76.784f, 125.636f) curveTo(75.395f, 125.114f, 74.04f, 124.626f, 72.755f, 124.173f) curveTo(62.683f, 120.655f, 52.298f, 117.032f, 40.801f, 117.903f) curveTo(37.397f, 118.251f, 34.549f, 116.719f, 33.819f, 114.176f) curveTo(32.986f, 111.041f, 32.256f, 107.802f, 31.562f, 104.667f) curveTo(30.172f, 98.466f, 28.748f, 92.092f, 26.421f, 86.066f) curveTo(24.962f, 82.234f, 23.226f, 78.437f, 21.559f, 74.78f) curveTo(19.753f, 70.809f, 17.842f, 66.698f, 16.314f, 62.484f) curveTo(15.793f, 60.986f, 15.828f, 59.557f, 16.383f, 58.513f) curveTo(16.765f, 57.816f, 17.321f, 57.363f, 18.051f, 57.154f) curveTo(35.695f, 52.8f, 46.219f, 40.573f, 57.368f, 27.65f) curveTo(60.321f, 24.237f, 63.377f, 20.684f, 66.573f, 17.27f) curveTo(68.83f, 14.971f, 71.991f, 13.578f, 75.291f, 13.473f) curveTo(78.417f, 13.194f, 81.821f, 12.289f, 85.398f, 11.313f) curveTo(92.658f, 9.398f, 100.125f, 7.412f, 106.273f, 10.129f) curveTo(115.512f, 14.17f, 119.298f, 25.943f, 122.354f, 35.383f) curveTo(122.98f, 37.334f, 123.57f, 39.145f, 124.16f, 40.817f) curveTo(125.341f, 43.639f, 126.071f, 47.923f, 125.098f, 51.128f) curveTo(123.118f, 57.119f, 121.069f, 63.145f, 119.055f, 68.997f) curveTo(117.249f, 74.362f, 115.338f, 79.865f, 113.532f, 85.334f) curveTo(109.711f, 96.829f, 108.982f, 108.254f, 108.461f, 122.048f) curveTo(108.322f, 126.402f, 106.551f, 129.677f, 104.085f, 130.164f) curveTo(102.244f, 130.757f, 100.368f, 130.931f, 98.458f, 130.931f) close() moveTo(44.274f, 117.45f) curveTo(54.486f, 117.45f, 63.829f, 120.725f, 72.894f, 123.86f) curveTo(74.214f, 124.312f, 75.534f, 124.8f, 76.958f, 125.323f) curveTo(85.572f, 128.492f, 95.332f, 132.045f, 104.05f, 129.955f) curveTo(106.342f, 129.503f, 108.01f, 126.298f, 108.183f, 122.153f) curveTo(108.739f, 108.359f, 109.434f, 96.864f, 113.289f, 85.334f) curveTo(115.095f, 79.865f, 116.971f, 74.362f, 118.812f, 68.997f) curveTo(120.791f, 63.145f, 122.875f, 57.084f, 124.82f, 51.128f) curveTo(125.793f, 47.993f, 125.029f, 43.813f, 123.883f, 41.026f) curveTo(123.257f, 39.319f, 122.667f, 37.508f, 122.042f, 35.557f) curveTo(119.02f, 26.187f, 115.234f, 14.483f, 106.169f, 10.512f) curveTo(100.125f, 7.865f, 92.692f, 9.816f, 85.502f, 11.731f) curveTo(81.89f, 12.672f, 78.452f, 13.578f, 75.326f, 13.891f) curveTo(72.095f, 13.996f, 69.004f, 15.319f, 66.816f, 17.549f) curveTo(63.62f, 20.962f, 60.564f, 24.48f, 57.612f, 27.894f) curveTo(46.428f, 40.852f, 35.869f, 53.113f, 18.12f, 57.502f) curveTo(17.495f, 57.676f, 16.974f, 58.06f, 16.661f, 58.687f) curveTo(16.14f, 59.662f, 16.14f, 60.986f, 16.626f, 62.379f) curveTo(18.19f, 66.559f, 20.065f, 70.669f, 21.871f, 74.64f) curveTo(23.538f, 78.298f, 25.275f, 82.095f, 26.769f, 85.926f) curveTo(29.096f, 91.952f, 30.52f, 98.396f, 31.909f, 104.562f) curveTo(32.604f, 107.697f, 33.333f, 110.902f, 34.167f, 114.037f) curveTo(34.827f, 116.405f, 37.571f, 117.833f, 40.801f, 117.52f) curveTo(41.947f, 117.52f, 43.093f, 117.45f, 44.274f, 117.45f) close() } path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(99.187f, 121.804f) curveTo(89.809f, 121.7f, 80.709f, 119.296f, 71.887f, 116.928f) curveTo(63.169f, 114.594f, 54.138f, 112.19f, 44.83f, 112.086f) curveTo(41.843f, 112.156f, 39.203f, 110.484f, 38.439f, 107.941f) curveTo(37.953f, 106.234f, 37.466f, 104.457f, 37.015f, 102.751f) curveTo(35.348f, 96.62f, 33.611f, 90.245f, 30.937f, 84.394f) curveTo(29.513f, 81.293f, 27.915f, 78.263f, 26.317f, 75.337f) curveTo(24.65f, 72.202f, 22.913f, 68.962f, 21.42f, 65.653f) curveTo(20.864f, 64.33f, 20.829f, 63.041f, 21.316f, 62.1f) curveTo(21.663f, 61.438f, 22.219f, 61.02f, 22.983f, 60.881f) curveTo(39.168f, 57.711f, 49.206f, 44.788f, 58.897f, 32.248f) curveTo(61.432f, 28.974f, 64.037f, 25.595f, 66.712f, 22.495f) curveTo(69.595f, 19.917f, 72.269f, 17.757f, 76.02f, 17.061f) curveTo(78.174f, 16.538f, 80.744f, 15.563f, 83.453f, 14.553f) curveTo(90.435f, 11.94f, 98.354f, 9.014f, 104.258f, 11.174f) curveTo(112.768f, 14.309f, 116.068f, 25.073f, 118.707f, 33.711f) curveTo(119.159f, 35.174f, 119.576f, 36.602f, 120.027f, 37.856f) curveTo(120.965f, 40.713f, 121.833f, 44.997f, 120.548f, 49.212f) curveTo(119.402f, 52.591f, 118.221f, 56.004f, 117.075f, 59.279f) curveTo(110.719f, 77.497f, 104.71f, 94.704f, 103.772f, 114.246f) curveTo(103.703f, 118.635f, 101.827f, 121.665f, 99.187f, 121.804f) close() moveTo(44.865f, 111.738f) curveTo(54.208f, 111.842f, 63.238f, 114.28f, 71.991f, 116.614f) curveTo(80.779f, 118.948f, 89.844f, 121.386f, 99.187f, 121.456f) curveTo(101.584f, 121.352f, 103.355f, 118.391f, 103.494f, 114.246f) curveTo(104.432f, 94.669f, 110.441f, 77.427f, 116.797f, 59.174f) curveTo(117.943f, 55.9f, 119.124f, 52.521f, 120.27f, 49.142f) curveTo(121.521f, 45.032f, 120.687f, 40.817f, 119.749f, 37.996f) curveTo(119.333f, 36.707f, 118.881f, 35.313f, 118.43f, 33.816f) curveTo(115.825f, 25.247f, 112.56f, 14.553f, 104.189f, 11.488f) curveTo(98.423f, 9.363f, 90.539f, 12.289f, 83.627f, 14.866f) curveTo(80.918f, 15.877f, 78.313f, 16.852f, 76.159f, 17.374f) curveTo(72.478f, 18.036f, 69.838f, 20.196f, 66.99f, 22.704f) curveTo(64.315f, 25.804f, 61.71f, 29.183f, 59.175f, 32.457f) curveTo(49.449f, 45.032f, 39.377f, 58.025f, 23.052f, 61.195f) curveTo(22.427f, 61.334f, 21.941f, 61.682f, 21.628f, 62.24f) curveTo(21.177f, 63.076f, 21.246f, 64.26f, 21.732f, 65.479f) curveTo(23.191f, 68.788f, 24.928f, 71.993f, 26.63f, 75.128f) curveTo(28.193f, 78.089f, 29.825f, 81.119f, 31.249f, 84.219f) curveTo(33.924f, 90.106f, 35.695f, 96.446f, 37.362f, 102.611f) curveTo(37.814f, 104.318f, 38.3f, 106.06f, 38.786f, 107.767f) curveTo(39.516f, 110.205f, 42.016f, 111.807f, 44.865f, 111.738f) close() } path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(94.151f, 112.783f) horizontalLineTo(94.116f) curveTo(86.406f, 112.399f, 78.695f, 111.006f, 71.262f, 109.648f) curveTo(63.829f, 108.289f, 56.153f, 106.896f, 48.477f, 106.513f) curveTo(45.733f, 106.478f, 43.302f, 104.701f, 42.503f, 102.124f) curveTo(38.821f, 90.28f, 33.75f, 81.154f, 27.845f, 70.6f) lineTo(26.699f, 68.544f) curveTo(26.109f, 67.395f, 26.004f, 66.315f, 26.386f, 65.479f) curveTo(26.699f, 64.817f, 27.29f, 64.399f, 28.054f, 64.225f) curveTo(42.468f, 61.369f, 51.985f, 48.132f, 60.39f, 36.463f) curveTo(62.648f, 33.328f, 64.767f, 30.402f, 66.955f, 27.65f) curveTo(69.629f, 24.376f, 73.137f, 21.868f, 76.889f, 20.579f) curveTo(78.556f, 19.987f, 80.466f, 19.151f, 82.446f, 18.315f) curveTo(88.767f, 15.598f, 95.922f, 12.498f, 101.723f, 13.821f) curveTo(109.329f, 15.598f, 112.525f, 25.386f, 114.817f, 32.527f) curveTo(115.165f, 33.537f, 115.442f, 34.512f, 115.755f, 35.383f) curveTo(117.075f, 39.528f, 117.144f, 43.848f, 115.929f, 47.644f) curveTo(114.47f, 51.894f, 112.872f, 56.213f, 111.309f, 60.393f) curveTo(105.891f, 75.058f, 100.264f, 90.211f, 99.014f, 105.955f) curveTo(98.805f, 110.031f, 96.826f, 112.783f, 94.151f, 112.783f) close() moveTo(67.233f, 27.859f) curveTo(65.045f, 30.576f, 62.926f, 33.537f, 60.703f, 36.672f) curveTo(52.263f, 48.411f, 42.711f, 61.682f, 28.158f, 64.573f) curveTo(27.498f, 64.713f, 26.977f, 65.061f, 26.734f, 65.618f) curveTo(26.386f, 66.35f, 26.491f, 67.36f, 27.012f, 68.37f) lineTo(28.158f, 70.425f) curveTo(34.063f, 81.015f, 39.134f, 90.141f, 42.85f, 102.019f) curveTo(43.614f, 104.457f, 45.872f, 106.129f, 48.477f, 106.164f) curveTo(56.188f, 106.547f, 63.898f, 107.941f, 71.331f, 109.299f) curveTo(78.764f, 110.658f, 86.44f, 112.051f, 94.116f, 112.434f) horizontalLineTo(94.151f) curveTo(96.582f, 112.434f, 98.458f, 109.752f, 98.736f, 105.92f) curveTo(99.986f, 90.141f, 105.613f, 74.954f, 111.031f, 60.254f) curveTo(112.56f, 56.074f, 114.157f, 51.755f, 115.616f, 47.54f) curveTo(116.797f, 43.848f, 116.728f, 39.563f, 115.442f, 35.522f) curveTo(115.13f, 34.652f, 114.817f, 33.676f, 114.505f, 32.666f) curveTo(112.212f, 25.595f, 109.052f, 15.911f, 101.653f, 14.205f) curveTo(95.957f, 12.881f, 88.872f, 15.946f, 82.585f, 18.663f) curveTo(80.57f, 19.534f, 78.695f, 20.335f, 76.993f, 20.962f) curveTo(73.346f, 22.181f, 69.872f, 24.654f, 67.233f, 27.859f) close() } path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(89.288f, 104.179f) curveTo(89.254f, 104.179f, 89.219f, 104.179f, 89.184f, 104.179f) curveTo(82.967f, 103.9f, 76.68f, 103.238f, 70.602f, 102.576f) curveTo(64.524f, 101.915f, 58.237f, 101.253f, 52.054f, 100.974f) curveTo(49.241f, 100.904f, 46.879f, 99.198f, 46.08f, 96.62f) curveTo(43.128f, 87.354f, 38.786f, 80.91f, 33.785f, 73.456f) curveTo(33.264f, 72.655f, 32.708f, 71.854f, 32.152f, 71.018f) curveTo(31.562f, 70.077f, 31.388f, 69.171f, 31.666f, 68.405f) curveTo(31.909f, 67.743f, 32.534f, 67.256f, 33.403f, 67.012f) curveTo(45.768f, 63.842f, 54.243f, 51.546f, 61.71f, 40.643f) curveTo(63.62f, 37.891f, 65.392f, 35.244f, 67.198f, 32.875f) curveTo(70.15f, 29.009f, 73.763f, 26.083f, 77.653f, 24.376f) curveTo(78.903f, 23.853f, 80.223f, 23.226f, 81.612f, 22.565f) curveTo(87.031f, 19.987f, 93.144f, 17.096f, 98.597f, 18.001f) curveTo(105.335f, 19.151f, 108.183f, 26.361f, 110.441f, 32.178f) curveTo(110.684f, 32.771f, 110.892f, 33.363f, 111.136f, 33.92f) curveTo(112.525f, 37.961f, 112.525f, 42.559f, 111.066f, 46.843f) curveTo(109.468f, 51.406f, 107.662f, 56.004f, 105.752f, 60.846f) curveTo(100.959f, 73.003f, 96.027f, 85.578f, 94.498f, 97.944f) curveTo(93.943f, 101.706f, 91.893f, 104.179f, 89.288f, 104.179f) close() moveTo(67.406f, 33.084f) curveTo(65.6f, 35.453f, 63.829f, 38.065f, 61.919f, 40.817f) curveTo(54.416f, 51.755f, 45.907f, 64.12f, 33.437f, 67.325f) curveTo(32.673f, 67.534f, 32.152f, 67.917f, 31.944f, 68.475f) curveTo(31.701f, 69.102f, 31.874f, 69.938f, 32.395f, 70.774f) curveTo(32.951f, 71.61f, 33.507f, 72.411f, 34.028f, 73.212f) curveTo(39.064f, 80.701f, 43.406f, 87.18f, 46.393f, 96.481f) curveTo(47.157f, 98.919f, 49.38f, 100.521f, 52.054f, 100.591f) curveTo(58.272f, 100.87f, 64.558f, 101.531f, 70.637f, 102.193f) curveTo(76.715f, 102.855f, 83.002f, 103.517f, 89.184f, 103.796f) curveTo(89.219f, 103.796f, 89.254f, 103.796f, 89.288f, 103.796f) curveTo(91.65f, 103.796f, 93.595f, 101.357f, 94.081f, 97.874f) curveTo(95.645f, 85.438f, 100.577f, 72.864f, 105.37f, 60.672f) curveTo(107.28f, 55.83f, 109.086f, 51.267f, 110.684f, 46.704f) curveTo(112.108f, 42.489f, 112.108f, 37.996f, 110.753f, 34.025f) curveTo(110.545f, 33.467f, 110.302f, 32.91f, 110.059f, 32.283f) curveTo(107.836f, 26.57f, 105.057f, 19.43f, 98.493f, 18.315f) curveTo(93.178f, 17.409f, 87.1f, 20.3f, 81.716f, 22.843f) curveTo(80.327f, 23.505f, 78.973f, 24.132f, 77.722f, 24.654f) curveTo(73.901f, 26.396f, 70.324f, 29.287f, 67.406f, 33.084f) close() } path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(84.738f, 96.481f) curveTo(84.704f, 96.481f, 84.704f, 96.481f, 84.669f, 96.481f) curveTo(79.806f, 96.376f, 74.874f, 96.167f, 70.116f, 95.958f) curveTo(65.357f, 95.749f, 60.425f, 95.54f, 55.597f, 95.436f) curveTo(52.853f, 95.401f, 50.422f, 93.659f, 49.623f, 91.082f) curveTo(46.775f, 83.418f, 42.989f, 78.994f, 37.744f, 72.829f) curveTo(37.119f, 72.098f, 36.911f, 71.331f, 37.084f, 70.634f) curveTo(37.293f, 69.903f, 37.918f, 69.346f, 38.89f, 68.997f) curveTo(48.789f, 65.549f, 56.604f, 53.915f, 62.891f, 44.579f) curveTo(64.419f, 42.315f, 65.843f, 40.19f, 67.233f, 38.309f) curveTo(70.22f, 34.687f, 73.485f, 30.959f, 78.07f, 28.695f) curveTo(89.149f, 23.749f, 98.875f, 20.753f, 105.752f, 33.955f) curveTo(107.836f, 39.076f, 106.516f, 44.44f, 105.578f, 47.192f) curveTo(104.015f, 51.581f, 102.383f, 56.039f, 100.75f, 60.324f) curveTo(96.999f, 70.391f, 93.109f, 80.771f, 90.296f, 91.186f) curveTo(89.393f, 94.844f, 86.892f, 96.481f, 84.738f, 96.481f) close() moveTo(67.511f, 38.483f) curveTo(66.156f, 40.364f, 64.697f, 42.489f, 63.204f, 44.753f) curveTo(56.882f, 54.124f, 48.998f, 65.827f, 39.029f, 69.311f) curveTo(38.161f, 69.624f, 37.605f, 70.112f, 37.432f, 70.704f) curveTo(37.293f, 71.261f, 37.466f, 71.923f, 38.022f, 72.55f) curveTo(43.093f, 78.507f, 47.088f, 83.209f, 49.97f, 90.907f) curveTo(50.735f, 93.311f, 53.027f, 94.983f, 55.632f, 95.018f) curveTo(60.494f, 95.122f, 65.392f, 95.331f, 70.15f, 95.54f) curveTo(74.909f, 95.749f, 79.841f, 95.958f, 84.704f, 96.063f) curveTo(87.1f, 96.098f, 89.254f, 94.042f, 89.983f, 91.047f) curveTo(92.831f, 80.597f, 96.687f, 70.216f, 100.438f, 60.15f) curveTo(102.035f, 55.83f, 103.703f, 51.372f, 105.266f, 47.018f) curveTo(106.203f, 44.3f, 107.489f, 39.006f, 105.474f, 34.025f) curveTo(102.383f, 28.103f, 98.666f, 25.177f, 93.769f, 24.794f) curveTo(89.358f, 24.446f, 84.391f, 26.187f, 78.278f, 28.904f) curveTo(73.728f, 31.203f, 70.463f, 34.896f, 67.511f, 38.483f) close() } path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(80.674f, 90.002f) curveTo(77.027f, 90.002f, 73.346f, 90.002f, 69.699f, 90.002f) curveTo(66.191f, 90.002f, 62.683f, 90.002f, 59.175f, 90.002f) curveTo(56.604f, 90.002f, 54.069f, 88.434f, 52.992f, 86.17f) curveTo(50.144f, 80.945f, 47.331f, 77.287f, 43.579f, 73.943f) curveTo(43.024f, 73.386f, 42.746f, 72.829f, 42.781f, 72.271f) curveTo(42.85f, 71.47f, 43.51f, 70.6f, 44.83f, 69.694f) curveTo(52.714f, 63.981f, 58.688f, 55.656f, 64.489f, 47.61f) curveTo(65.392f, 46.356f, 66.26f, 45.136f, 67.163f, 43.917f) curveTo(70.116f, 40.225f, 73.485f, 36.08f, 78.209f, 33.816f) curveTo(85.919f, 31.168f, 92.345f, 29.67f, 99.535f, 36.184f) lineTo(99.569f, 36.219f) curveTo(102.035f, 39.981f, 100.924f, 45.241f, 99.569f, 49.003f) lineTo(97.485f, 54.82f) curveTo(93.977f, 64.643f, 90.365f, 74.78f, 86.926f, 84.846f) curveTo(85.884f, 88.086f, 83.557f, 90.002f, 80.674f, 90.002f) close() moveTo(67.511f, 44.057f) curveTo(66.642f, 45.276f, 65.739f, 46.495f, 64.836f, 47.749f) curveTo(59.001f, 55.83f, 52.992f, 64.19f, 45.073f, 69.938f) curveTo(43.857f, 70.809f, 43.232f, 71.54f, 43.163f, 72.271f) curveTo(43.128f, 72.759f, 43.336f, 73.212f, 43.823f, 73.665f) curveTo(47.574f, 77.044f, 50.422f, 80.736f, 53.305f, 85.996f) curveTo(54.312f, 88.121f, 56.743f, 89.618f, 59.175f, 89.618f) curveTo(62.683f, 89.618f, 66.191f, 89.618f, 69.699f, 89.618f) curveTo(73.346f, 89.618f, 76.993f, 89.618f, 80.674f, 89.618f) curveTo(83.418f, 89.618f, 85.572f, 87.807f, 86.649f, 84.637f) curveTo(90.052f, 74.605f, 93.7f, 64.434f, 97.207f, 54.611f) lineTo(99.291f, 48.794f) curveTo(100.611f, 45.102f, 101.688f, 39.981f, 99.326f, 36.324f) curveTo(92.275f, 29.949f, 86.197f, 31.342f, 78.382f, 34.025f) curveTo(73.763f, 36.289f, 70.428f, 40.399f, 67.511f, 44.057f) close() } path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(62.196f, 84.812f) curveTo(59.73f, 84.812f, 57.368f, 83.558f, 56.292f, 81.607f) curveTo(54.764f, 78.681f, 52.471f, 75.72f, 50.04f, 73.456f) curveTo(49.554f, 73.003f, 49.31f, 72.446f, 49.31f, 71.854f) curveTo(49.31f, 70.739f, 50.109f, 69.415f, 51.811f, 67.778f) curveTo(57.334f, 62.483f, 62.023f, 56.213f, 66.573f, 50.187f) lineTo(67.233f, 49.282f) lineTo(67.511f, 48.933f) curveTo(70.463f, 45.345f, 73.52f, 41.618f, 78.0f, 39.946f) curveTo(83.419f, 38.832f, 87.864f, 38.205f, 92.414f, 40.956f) curveTo(95.575f, 43.778f, 94.116f, 49.386f, 92.97f, 52.451f) lineTo(92.484f, 53.775f) curveTo(89.775f, 61.369f, 86.996f, 69.206f, 84.287f, 76.974f) curveTo(83.523f, 79.97f, 80.57f, 83.314f, 77.444f, 83.558f) curveTo(74.978f, 83.767f, 72.512f, 83.941f, 70.046f, 84.15f) curveTo(67.58f, 84.359f, 65.149f, 84.568f, 62.683f, 84.742f) curveTo(62.509f, 84.812f, 62.335f, 84.812f, 62.196f, 84.812f) close() moveTo(67.476f, 49.56f) lineTo(66.816f, 50.466f) curveTo(62.266f, 56.527f, 57.577f, 62.797f, 52.02f, 68.126f) curveTo(50.457f, 69.659f, 49.658f, 70.913f, 49.658f, 71.923f) curveTo(49.658f, 72.446f, 49.866f, 72.864f, 50.248f, 73.247f) curveTo(52.714f, 75.511f, 55.007f, 78.507f, 56.57f, 81.467f) curveTo(57.646f, 83.418f, 60.147f, 84.637f, 62.648f, 84.463f) curveTo(65.114f, 84.289f, 67.545f, 84.08f, 70.011f, 83.871f) curveTo(72.478f, 83.662f, 74.943f, 83.453f, 77.41f, 83.279f) curveTo(80.362f, 83.07f, 83.21f, 79.796f, 83.939f, 76.939f) curveTo(86.649f, 69.171f, 89.427f, 61.334f, 92.136f, 53.74f) lineTo(92.623f, 52.417f) curveTo(93.734f, 49.421f, 95.158f, 43.952f, 92.171f, 41.305f) curveTo(87.76f, 38.657f, 83.349f, 39.25f, 78.07f, 40.364f) curveTo(73.728f, 41.967f, 70.671f, 45.659f, 67.754f, 49.247f) lineTo(67.476f, 49.56f) close() } path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(64.836f, 79.029f) curveTo(62.648f, 79.029f, 60.703f, 78.054f, 59.8f, 76.417f) curveTo(58.723f, 74.431f, 58.133f, 73.525f, 56.847f, 71.888f) curveTo(55.632f, 70.286f, 56.431f, 67.778f, 59.244f, 64.643f) curveTo(61.71f, 61.856f, 64.28f, 58.721f, 67.094f, 55.064f) curveTo(69.386f, 52.068f, 72.964f, 47.993f, 77.444f, 46.948f) curveTo(80.466f, 46.599f, 81.925f, 46.599f, 84.669f, 47.366f) horizontalLineTo(84.704f) curveTo(87.934f, 49.038f, 86.822f, 53.427f, 86.093f, 56.318f) curveTo(85.989f, 56.666f, 85.919f, 57.014f, 85.85f, 57.293f) curveTo(84.426f, 61.299f, 83.002f, 65.305f, 81.612f, 69.276f) curveTo(80.223f, 73.351f, 77.514f, 76.277f, 74.318f, 77.078f) curveTo(72.755f, 77.427f, 71.574f, 77.705f, 70.394f, 77.949f) curveTo(69.213f, 78.228f, 68.032f, 78.472f, 66.503f, 78.82f) curveTo(65.948f, 78.959f, 65.392f, 79.029f, 64.836f, 79.029f) close() moveTo(80.431f, 47.052f) curveTo(79.563f, 47.052f, 78.625f, 47.122f, 77.479f, 47.261f) curveTo(73.137f, 48.271f, 69.629f, 52.312f, 67.372f, 55.238f) curveTo(64.558f, 58.896f, 61.988f, 62.065f, 59.522f, 64.852f) curveTo(56.847f, 67.848f, 56.049f, 70.216f, 57.16f, 71.645f) curveTo(58.445f, 73.316f, 59.036f, 74.222f, 60.147f, 76.242f) curveTo(61.189f, 78.158f, 63.794f, 79.099f, 66.469f, 78.507f) curveTo(68.032f, 78.158f, 69.213f, 77.914f, 70.359f, 77.636f) curveTo(71.54f, 77.357f, 72.721f, 77.113f, 74.249f, 76.765f) curveTo(77.305f, 75.999f, 79.945f, 73.142f, 81.3f, 69.171f) curveTo(82.689f, 65.165f, 84.113f, 61.194f, 85.537f, 57.189f) curveTo(85.607f, 56.91f, 85.676f, 56.562f, 85.78f, 56.213f) curveTo(86.475f, 53.427f, 87.552f, 49.212f, 84.565f, 47.679f) curveTo(82.932f, 47.226f, 81.751f, 47.052f, 80.431f, 47.052f) close() } path( fill = SolidColor(Color.Black), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(67.997f, 71.993f) curveTo(66.121f, 71.993f, 64.593f, 71.192f, 63.898f, 69.729f) curveTo(62.891f, 67.639f, 63.968f, 64.504f, 66.92f, 60.846f) curveTo(70.394f, 56.562f, 73.589f, 54.402f, 76.715f, 54.228f) curveTo(77.583f, 54.298f, 78.243f, 54.716f, 78.695f, 55.412f) curveTo(79.667f, 56.91f, 79.598f, 59.557f, 78.556f, 62.553f) curveTo(77.062f, 66.768f, 74.284f, 70.007f, 70.88f, 71.401f) curveTo(69.872f, 71.819f, 68.9f, 71.993f, 67.997f, 71.993f) close() moveTo(76.715f, 54.611f) curveTo(73.728f, 54.785f, 70.602f, 56.91f, 67.198f, 61.09f) curveTo(64.315f, 64.643f, 63.238f, 67.639f, 64.211f, 69.59f) curveTo(65.184f, 71.575f, 67.893f, 72.202f, 70.776f, 71.087f) curveTo(74.075f, 69.729f, 76.819f, 66.559f, 78.243f, 62.449f) curveTo(79.251f, 59.592f, 79.32f, 56.98f, 78.417f, 55.621f) curveTo(78.0f, 54.994f, 77.444f, 54.646f, 76.715f, 54.611f) close() } } return wireframe!! } private var wireframe: ImageVector? = null ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/exercises/Exercises.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.exercises import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.looker.kenko.R import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.ExercisesPreviewParameter import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.ui.components.BackButton import com.looker.kenko.ui.components.ErrorSnackbar import com.looker.kenko.ui.components.KenkoBorderWidth import com.looker.kenko.ui.components.LazyTargets import com.looker.kenko.ui.components.SecondaryKenkoButton import com.looker.kenko.ui.components.SwipeToDeleteBox import com.looker.kenko.ui.components.TargetChip import com.looker.kenko.ui.extensions.plus import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme @Composable fun Exercises( viewModel: ExercisesViewModel, onExerciseClick: (id: Int?) -> Unit, onCreateClick: (target: MuscleGroups?) -> Unit, onBackPress: () -> Unit, ) { val state by viewModel.exercises.collectAsStateWithLifecycle() Exercises( state = state, snackbarState = viewModel.snackbarState, onBackPress = onBackPress, onExerciseClick = onExerciseClick, onCreateClick = onCreateClick, onSelectTarget = viewModel::setTarget, onReferenceClick = viewModel::onReferenceClick, onRemove = viewModel::removeExercise, ) } @Composable private fun Exercises( state: ExercisesUiState, snackbarState: SnackbarHostState, onExerciseClick: (id: Int?) -> Unit, onCreateClick: (target: MuscleGroups?) -> Unit, onSelectTarget: (MuscleGroups?) -> Unit, onRemove: (Int?) -> Unit, onBackPress: () -> Unit, onReferenceClick: (String) -> Unit, modifier: Modifier = Modifier, ) { Scaffold( modifier = modifier.fillMaxWidth(), floatingActionButton = { SecondaryKenkoButton( onClick = { onCreateClick(state.selected) }, label = { Icon( painter = KenkoIcons.Add, contentDescription = null, ) }, icon = { Text(stringResource(R.string.label_create_exercise)) } ) }, floatingActionButtonPosition = FabPosition.Center, snackbarHost = { SnackbarHost(hostState = snackbarState) { ErrorSnackbar(data = it) } }, topBar = { Header( target = state.selected, onSelect = onSelectTarget, onBackPress = onBackPress, ) }, ) { innerPadding -> ExercisesList( exercises = state.exercises, contentPadding = innerPadding + PaddingValues(bottom = 80.dp), onExerciseClick = onExerciseClick, onReferenceClick = onReferenceClick, onRemove = onRemove, ) } } @Composable private fun ExercisesList( exercises: List, contentPadding: PaddingValues, onExerciseClick: (id: Int?) -> Unit, onRemove: (Int?) -> Unit, onReferenceClick: (String) -> Unit, ) { LazyColumn( contentPadding = contentPadding, ) { items(exercises, key = { it.id!! }) { exercise -> val exerciseId by rememberUpdatedState(exercise.id) SwipeToDeleteBox( modifier = Modifier.animateItem(), onDismiss = { onRemove(exerciseId) } ) { ExerciseItem( exercise = exercise, onClick = { onExerciseClick(exerciseId) }, referenceButton = { if (exercise.reference != null) { FilledTonalIconButton( modifier = Modifier.size(56.dp), shape = MaterialTheme.shapes.extraLarge, onClick = { onReferenceClick(exercise.reference) } ) { Icon(painter = KenkoIcons.Lightbulb, contentDescription = null) } } } ) } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun Header( target: MuscleGroups?, onSelect: (MuscleGroups?) -> Unit, onBackPress: () -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier.background(MaterialTheme.colorScheme.background)) { TopAppBar( title = { Text(text = stringResource(id = R.string.label_browse_exercises)) }, navigationIcon = { BackButton(onClick = onBackPress) } ) LazyTargets(contentPadding = PaddingValues(horizontal = 8.dp)) { TargetChip( selected = target == it, onClick = { onSelect(it) }, text = stringResource(it.string), ) } HorizontalDivider(thickness = KenkoBorderWidth) } } @Composable private fun ExerciseItem( exercise: Exercise, onClick: () -> Unit, referenceButton: @Composable () -> Unit, modifier: Modifier = Modifier, ) { @StringRes val targetName: Int = remember { exercise.target.stringRes } Surface( modifier = modifier, onClick = onClick, ) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { Column( modifier = Modifier.weight(1f), ) { Text( text = exercise.name, style = MaterialTheme.typography.titleMedium, ) Text( text = stringResource(targetName), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline, ) } Box(modifier = Modifier.size(56.dp)) { referenceButton() } } } } @Preview @Composable private fun ExercisesPreview( @PreviewParameter(ExercisesPreviewParameter::class, limit = 2) exercises: List, ) { KenkoTheme { Exercises( state = ExercisesUiState(MuscleGroups.entries.flatMap { exercises }), snackbarState = SnackbarHostState(), onExerciseClick = {}, onCreateClick = {}, onSelectTarget = {}, onBackPress = {}, onReferenceClick = {}, onRemove = {} ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/exercises/ExercisesViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.exercises import androidx.annotation.StringRes import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Stable import androidx.compose.ui.platform.UriHandler import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.looker.kenko.R import com.looker.kenko.data.StringHandler import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.data.repository.ExerciseRepo import com.looker.kenko.utils.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class ExercisesViewModel @Inject constructor( private val repo: ExerciseRepo, private val uriHandler: UriHandler, private val stringHandler: StringHandler, ) : ViewModel() { // null -> all private val selectedTarget: MutableStateFlow = MutableStateFlow(null) private val exercisesStream: Flow> = repo.stream val snackbarState = SnackbarHostState() val exercises: StateFlow = combine( exercisesStream, selectedTarget, ) { exercises, target -> val selectedExercises = if (target == null) { exercises } else { exercises.filter { it.target == target } } ExercisesUiState( exercises = selectedExercises, selected = target, ) }.asStateFlow(ExercisesUiState()) fun removeExercise(id: Int?) { viewModelScope.launch { if (id == null) { snackbarState.showSnackbar(stringHandler.getString(R.string.error_unknown)) return@launch } repo.remove(id) } } fun setTarget(value: MuscleGroups?) { viewModelScope.launch { selectedTarget.emit(value) } } fun onReferenceClick(reference: String) { viewModelScope.launch { try { uriHandler.openUri(reference) } catch (e: IllegalStateException) { snackbarState.showSnackbar( e.message ?: stringHandler.getString(R.string.error_invalid_url) ) } } } } val MuscleGroups?.string: Int @StringRes get() = this?.stringRes ?: R.string.label_all_muscle_groups @Stable class ExercisesUiState( val exercises: List = emptyList(), val selected: MuscleGroups? = null, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/exercises/navigation/ExercisesNavigation.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.exercises.navigation import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.ui.exercises.Exercises import kotlinx.serialization.Serializable @Serializable object ExercisesRoute fun NavController.navigateToExercises(navOptions: NavOptions? = null) { navigate(ExercisesRoute, navOptions = navOptions) } fun NavGraphBuilder.exercises( onExerciseClick: (id: Int?) -> Unit, onCreateClick: (target: MuscleGroups?) -> Unit, onBackPress: () -> Unit, ) { composable { Exercises( onExerciseClick = onExerciseClick, onCreateClick = onCreateClick, onBackPress = onBackPress, viewModel = hiltViewModel(), ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/extensions/Modifier.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.extensions import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.layout.layout fun Modifier.vertical(towardsRight: Boolean = true) = layout { measurable, constraints -> val placeable = measurable.measure(constraints) layout(placeable.height, placeable.width) { placeable.place( x = -(placeable.width / 2 - placeable.height / 2), y = -(placeable.height / 2 - placeable.width / 2) ) } }.rotate(90F * (if (towardsRight) 1 else -1)) const val PHI = 16F / 10F ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/extensions/PaddingValues.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.extensions import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.LayoutDirection @Composable operator fun PaddingValues.plus(other: PaddingValues): PaddingValues { val layoutDirection: LayoutDirection = LocalLayoutDirection.current return PaddingValues( start = calculateStartPadding(layoutDirection) + other.calculateStartPadding(layoutDirection), end = calculateEndPadding(layoutDirection) + other.calculateEndPadding(layoutDirection), top = calculateTopPadding() + other.calculateTopPadding(), bottom = calculateBottomPadding() + other.calculateBottomPadding() ) } @Composable operator fun PaddingValues.minus(other: PaddingValues): PaddingValues { val layoutDirection: LayoutDirection = LocalLayoutDirection.current return PaddingValues( start = calculateStartPadding(layoutDirection) - other.calculateStartPadding(layoutDirection), end = calculateEndPadding(layoutDirection) - other.calculateEndPadding(layoutDirection), top = calculateTopPadding() - other.calculateTopPadding(), bottom = calculateBottomPadding() - other.calculateBottomPadding() ) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/extensions/String.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.extensions import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @Composable fun normalizeInt(value: Int, padding: Char = '0', length: Int = 2): String { return remember(value) { value.toString().padStart(length, padding) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/getStarted/GetStarted.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.getStarted import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp import com.looker.kenko.R import com.looker.kenko.ui.components.HealthQuotes import com.looker.kenko.ui.components.TickerText import com.looker.kenko.ui.components.TypingText import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme import com.looker.kenko.ui.theme.header import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.milliseconds @Composable fun GetStarted(onNext: () -> Unit) { GetStarted(onNextClick = onNext) } @Composable private fun GetStarted( modifier: Modifier = Modifier, onNextClick: () -> Unit, ) { Surface( modifier = modifier.fillMaxSize(), ) { val iconVisibility = remember { Animatable(-50F) } val buttonVisibility = remember { Animatable(0.75F) } LaunchedEffect(true) { buttonVisibility.animateTo( targetValue = 0.85F, animationSpec = spring(), ) launch { iconVisibility.animateTo( targetValue = 0F, animationSpec = spring( stiffness = Spring.StiffnessVeryLow, dampingRatio = Spring.DampingRatioMediumBouncy, ), ) } launch { buttonVisibility.animateTo( targetValue = 1F, animationSpec = spring( stiffness = Spring.StiffnessVeryLow, dampingRatio = Spring.DampingRatioMediumBouncy, ), ) } } Column( modifier = Modifier .fillMaxSize(), ) { var startShowingFirstMeaning by remember { mutableStateOf(false) } var startShowingSecondMeaning by remember { mutableStateOf(false) } Spacer(modifier = Modifier.height(80.dp)) Text( text = stringResource(R.string.label_kenko), style = MaterialTheme.typography.header(), color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(horizontal = 18.dp), ) TypingText( text = stringResource(R.string.label_kenko_jp), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.secondary, typingDelay = 10.milliseconds, onCompleteListener = { startShowingFirstMeaning = true }, modifier = Modifier.padding(horizontal = 18.dp), ) TypingText( text = stringResource(R.string.label_kenko_meaning), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.outline, startTyping = startShowingFirstMeaning, typingDelay = 10.milliseconds, initialDelay = 0.milliseconds, onCompleteListener = { startShowingSecondMeaning = true }, modifier = Modifier.padding(horizontal = 18.dp), ) TypingText( text = stringResource(R.string.label_kenko_meaning_ALT), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.outline, startTyping = startShowingSecondMeaning, typingDelay = 10.milliseconds, initialDelay = 0.milliseconds, modifier = Modifier.padding(horizontal = 18.dp), ) Spacer(modifier = Modifier.weight(1F)) TickerText( texts = stringArrayResource(R.array.label_features), color = MaterialTheme.colorScheme.onTertiaryContainer, modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer), ) Column( modifier = Modifier .fillMaxWidth() .fillMaxHeight(0.6F) .background(MaterialTheme.colorScheme.primaryContainer) .navigationBarsPadding(), verticalArrangement = Arrangement.Bottom, horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.weight(1F)) Text( text = stringResource(R.string.label_boarding_quote), style = MaterialTheme.typography.displaySmall, color = MaterialTheme.colorScheme.onPrimaryContainer, textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.weight(1F)) Button( modifier = Modifier .graphicsLayer { scaleX = buttonVisibility.value scaleY = buttonVisibility.value translationY = (1F - buttonVisibility.value) * 15F }, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.inverseSurface, contentColor = MaterialTheme.colorScheme.inverseOnSurface, ), onClick = onNextClick, contentPadding = PaddingValues( vertical = 24.dp, horizontal = 40.dp, ), ) { ButtonIcon( modifier = Modifier .graphicsLayer { translationX = iconVisibility.value rotationZ = iconVisibility.value }, backgroundColor = MaterialTheme.colorScheme.primaryContainer, iconColor = MaterialTheme.colorScheme.onPrimaryContainer, ) Spacer(modifier = Modifier.width(8.dp)) Text(text = stringResource(R.string.label_lets_go)) } HealthQuotes( modifier = Modifier.align(Alignment.CenterHorizontally), color = MaterialTheme.colorScheme.outline, ) } } } } @Composable private fun ButtonIcon( iconColor: Color, backgroundColor: Color, modifier: Modifier = Modifier, ) { Icon( modifier = modifier .background(backgroundColor, CircleShape) .padding(8.dp), painter = KenkoIcons.ArrowForward, tint = iconColor, contentDescription = "", ) } @Preview @PreviewScreenSizes @Composable private fun GetStartedPreview() { KenkoTheme { GetStarted(onNextClick = {}) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/getStarted/GetStartedButton.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.getStarted import androidx.compose.foundation.layout.Box import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.unit.dp import com.looker.kenko.ui.theme.KenkoIcons @Composable fun ButtonGroup( content: @Composable () -> Unit, modifier: Modifier = Modifier, ) { Layout( modifier = modifier, content = { Icon( modifier = Modifier.Companion.layoutId(ButtonID.Cloud), imageVector = KenkoIcons.Cloud, contentDescription = null, ) Icon( modifier = Modifier.Companion.layoutId(ButtonID.Arrow1), imageVector = KenkoIcons.Arrow1, contentDescription = null, ) Icon( modifier = Modifier.Companion.layoutId(ButtonID.Arrow2), imageVector = KenkoIcons.Arrow2, contentDescription = null, ) Icon( modifier = Modifier.Companion.layoutId(ButtonID.Arrow3), imageVector = KenkoIcons.Arrow3, contentDescription = null, ) Icon( modifier = Modifier.Companion.layoutId(ButtonID.Arrow4), imageVector = KenkoIcons.Arrow4, contentDescription = null, ) Box(modifier = Modifier.layoutId(ButtonID.Button)) { content() } }, ) { measurables, constraints -> lateinit var cloud: Measurable lateinit var arrow1: Measurable lateinit var arrow2: Measurable lateinit var arrow3: Measurable lateinit var arrow4: Measurable lateinit var button: Measurable measurables.forEach { measurable -> when (measurable.layoutId) { ButtonID.Button -> button = measurable ButtonID.Cloud -> cloud = measurable ButtonID.Arrow1 -> arrow1 = measurable ButtonID.Arrow2 -> arrow2 = measurable ButtonID.Arrow3 -> arrow3 = measurable ButtonID.Arrow4 -> arrow4 = measurable else -> error("Unknown Element") } } val cloudPlaceable = cloud.measure(constraints) val arrow1Placeable = arrow1.measure(constraints) val arrow2Placeable = arrow2.measure(constraints) val arrow3Placeable = arrow3.measure(constraints) val arrow4Placeable = arrow4.measure(constraints) val buttonPlaceable = button.measure(constraints) val width = 360.dp.toPx().toInt() layout(width, 162.dp.toPx().toInt()) { val x = (width / 2) - (buttonPlaceable.width / 2) cloudPlaceable.placeRelative(16.dp.toPx().toInt(), 31.dp.toPx().toInt()) arrow1Placeable.placeRelative(148.dp.toPx().toInt(), 2.dp.toPx().toInt()) arrow2Placeable.placeRelative(200.dp.toPx().toInt(), 0.dp.toPx().toInt()) arrow3Placeable.placeRelative(270.dp.toPx().toInt(), 26.dp.toPx().toInt()) arrow4Placeable.placeRelative(290.dp.toPx().toInt(), 100.dp.toPx().toInt()) buttonPlaceable.placeRelative(x, 68.dp.toPx().toInt()) } } } private enum class ButtonID { Cloud, Arrow1, Arrow2, Arrow3, Arrow4, Button } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/getStarted/GetStartedOld.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.getStarted import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.looker.kenko.R import com.looker.kenko.ui.components.HealthQuotes import com.looker.kenko.ui.components.TypingText import com.looker.kenko.ui.extensions.PHI import com.looker.kenko.ui.extensions.vertical import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable fun GetStartedOld(onNext: () -> Unit) { val viewModel: GetStartedOldViewModel = hiltViewModel() val isOnboardingDone = viewModel.isOnboardingDone val updatedOnNext by rememberUpdatedState(newValue = onNext) LaunchedEffect(Unit) { if (isOnboardingDone) { delay(300) updatedOnNext() } } GetStarted( isOnboardingDone = isOnboardingDone, onNextClick = updatedOnNext, ) } @Composable private fun GetStarted( isOnboardingDone: Boolean, onNextClick: () -> Unit, ) { Surface { Box( modifier = Modifier.fillMaxSize(), ) { val iconVisibility = remember { Animatable(-50F) } val buttonVisibility = remember { Animatable(0.75F) } LaunchedEffect(true) { buttonVisibility.animateTo( targetValue = 0.85F, animationSpec = spring(), ) launch { iconVisibility.animateTo( targetValue = 0F, animationSpec = spring( stiffness = Spring.StiffnessVeryLow, dampingRatio = Spring.DampingRatioMediumBouncy, ), ) } buttonVisibility.animateTo( targetValue = 1F, animationSpec = spring( stiffness = Spring.StiffnessVeryLow, dampingRatio = Spring.DampingRatioMediumBouncy, ), ) } Icon( modifier = Modifier .align(Alignment.TopEnd) .offset(x = 50.dp, y = 56.dp) .graphicsLayer { translationX = iconVisibility.value * 2 rotationZ = iconVisibility.value }, imageVector = KenkoIcons.Dawn, tint = MaterialTheme.colorScheme.secondary, contentDescription = null, ) HeroTitle(modifier = Modifier.align(Alignment.CenterStart)) Column( modifier = Modifier .align(Alignment.BottomCenter) .navigationBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { if (!isOnboardingDone) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.secondary) { ButtonGroup( content = { FilledTonalButton( modifier = Modifier .graphicsLayer { scaleX = buttonVisibility.value scaleY = buttonVisibility.value translationY = (1F - buttonVisibility.value) * 15F }, onClick = onNextClick, contentPadding = PaddingValues( vertical = 24.dp, horizontal = 40.dp, ), ) { ButtonIcon( modifier = Modifier .graphicsLayer { translationX = iconVisibility.value rotationZ = iconVisibility.value }, ) Spacer(modifier = Modifier.width(8.dp)) TypingText(text = stringResource(R.string.label_lets_go)) } }, ) } } Spacer(modifier = Modifier.height(8.dp)) HealthQuotes() Spacer(modifier = Modifier.height(4.dp)) } } } } @Composable private fun HeroTitle(modifier: Modifier = Modifier) { Row(modifier, horizontalArrangement = Arrangement.spacedBy((-12).dp)) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text( modifier = Modifier.vertical(), text = stringResource(R.string.label_kenko).uppercase(), style = MaterialTheme.typography.displayLarge, color = MaterialTheme.colorScheme.tertiary, ) Box( modifier = Modifier .width(48.dp) .aspectRatio(1 / PHI) .border(2.dp, MaterialTheme.colorScheme.outline, CircleShape), ) } Column { var startShowingFirstMeaning by remember { mutableStateOf(false) } var startShowingSecondMeaning by remember { mutableStateOf(false) } TypingText( text = stringResource(R.string.label_kenko_jp), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary, typingDelay = 10.milliseconds, onCompleteListener = { startShowingFirstMeaning = true }, ) TypingText( text = stringResource(R.string.label_kenko_meaning), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.secondary, startTyping = startShowingFirstMeaning, typingDelay = 10.milliseconds, initialDelay = 0.milliseconds, onCompleteListener = { startShowingSecondMeaning = true }, ) TypingText( text = stringResource(R.string.label_kenko_meaning_ALT), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.secondary, startTyping = startShowingSecondMeaning, typingDelay = 10.milliseconds, initialDelay = 0.milliseconds, ) } } } @Composable private fun ButtonIcon( modifier: Modifier = Modifier, ) { Icon( modifier = modifier .background(MaterialTheme.colorScheme.onSecondaryContainer, CircleShape) .padding(8.dp), painter = KenkoIcons.ArrowForward, tint = MaterialTheme.colorScheme.secondaryContainer, contentDescription = "", ) } @Preview @PreviewScreenSizes @Composable private fun GetStartedPreview() { KenkoTheme { GetStarted(isOnboardingDone = false, onNextClick = {}) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/getStarted/GetStartedOldViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.getStarted import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.navigation.toRoute import com.looker.kenko.ui.getStarted.navigation.GetStartedRoute import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel class GetStartedOldViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel() { private val routeData: GetStartedRoute = savedStateHandle.toRoute() val isOnboardingDone = routeData.isOnboardingDone } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/getStarted/navigation/GetStartedNavigation.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.getStarted.navigation import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.looker.kenko.ui.getStarted.GetStartedOld import kotlinx.serialization.Serializable @Serializable data class GetStartedRoute(val isOnboardingDone: Boolean) fun NavGraphBuilder.getStarted(onNext: () -> Unit) { composable { GetStartedOld(onNext) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/home/Home.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.home import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Alignment.Companion.TopEnd import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.LineBreak import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.looker.kenko.R import com.looker.kenko.ui.components.KenkoBorderWidth import com.looker.kenko.ui.components.LiftingQuotes import com.looker.kenko.ui.components.TertiaryKenkoButton import com.looker.kenko.ui.components.TickerText import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme import com.looker.kenko.ui.theme.header @Composable fun Home( viewModel: HomeViewModel, onProfileClick: () -> Unit, onSelectPlanClick: () -> Unit, onAddExerciseClick: () -> Unit, onExploreSessionsClick: () -> Unit, onExploreExercisesClick: () -> Unit, onStartSessionClick: () -> Unit, onCurrentPlanClick: (Int) -> Unit, ) { val state by viewModel.state.collectAsStateWithLifecycle() Home( state = state, onProfileClick = onProfileClick, onSelectPlanClick = onSelectPlanClick, onAddExerciseClick = onAddExerciseClick, onExploreSessionsClick = onExploreSessionsClick, onExploreExercisesClick = onExploreExercisesClick, onStartSessionClick = onStartSessionClick, onCurrentPlanClick = onCurrentPlanClick, ) } // TODO: Add current plan indicator on this page @Composable private fun Home( state: HomeUiData, onProfileClick: () -> Unit = {}, onSelectPlanClick: () -> Unit = {}, onAddExerciseClick: () -> Unit = {}, onExploreSessionsClick: () -> Unit = {}, onExploreExercisesClick: () -> Unit = {}, onStartSessionClick: () -> Unit = {}, onCurrentPlanClick: (Int) -> Unit = {}, ) { Scaffold( topBar = { KenkoTopBar { FilledTonalIconButton(onClick = onProfileClick) { Icon(painter = KenkoIcons.Person, contentDescription = null) } } }, ) { innerPadding -> Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .padding(innerPadding), ) { HorizontalDivider(thickness = KenkoBorderWidth) AnimatedContent( modifier = Modifier.align(CenterHorizontally), targetState = state.isPlanSelected, label = "", ) { isPlanActive -> if (isPlanActive) { Row( modifier = Modifier .widthIn(240.dp, 420.dp) .height(120.dp), ) { ExploreExerciseCard( onClick = onExploreExercisesClick, onLongClick = onAddExerciseClick, modifier = Modifier.weight(1F), ) VerticalDivider() SessionHistoryCard( onClick = onExploreSessionsClick, modifier = Modifier.weight(1F), ) } } else { TickerText( text = stringResource(R.string.label_select_a_plan), color = MaterialTheme.colorScheme.outline, ) } } HorizontalDivider(thickness = KenkoBorderWidth) if (state.isPlanSelected) { StartSession( onStartSessionClick = { if (state.isTodayEmpty) { onCurrentPlanClick(state.currentPlanId!!) } else { onStartSessionClick() } }, content = { val heading = remember(state.isSessionStarted, state.isTodayEmpty) { if (state.isTodayEmpty) { R.string.label_nothing_today } else if (state.isSessionStarted) { R.string.label_continue_session_heading } else { if (state.isFirstSession) { R.string.label_start_first_session } else { R.string.label_start_session_heading } } } Text( modifier = Modifier .align(CenterHorizontally) .padding(horizontal = 16.dp), text = stringResource(heading), style = MaterialTheme.typography.header() .merge( lineBreak = LineBreak.Heading, color = MaterialTheme.colorScheme.primary, ), ) }, buttonText = { val stringRes = remember(state.isSessionStarted, state.isTodayEmpty) { if (state.isTodayEmpty) { R.string.label_edit_plan } else if (state.isSessionStarted) { R.string.label_continue_session } else { R.string.label_start_session } } Text(text = stringResource(stringRes)) }, ) } else { SelectPlan(onSelectPlanClick = onSelectPlanClick) } LiftingQuotes(Modifier.align(CenterHorizontally)) } } } @Composable private fun ColumnScope.StartSession( onStartSessionClick: () -> Unit, content: @Composable () -> Unit, buttonText: @Composable () -> Unit, ) { Spacer(modifier = Modifier.weight(1F)) content() Spacer(modifier = Modifier.weight(1F)) TertiaryKenkoButton( modifier = Modifier.align(CenterHorizontally), onClick = onStartSessionClick, label = buttonText, icon = { Icon( modifier = Modifier.size(18.dp), painter = KenkoIcons.ArrowOutward, contentDescription = null, ) }, ) } @Composable private fun ColumnScope.SelectPlan( onSelectPlanClick: () -> Unit, ) { Spacer(modifier = Modifier.weight(1F)) Text( modifier = Modifier .align(CenterHorizontally) .padding(horizontal = 16.dp), text = stringResource(R.string.label_selecting_a_plan), style = MaterialTheme.typography.header().copy( lineBreak = LineBreak.Heading, ), color = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.weight(1F)) Button( modifier = Modifier.align(CenterHorizontally), onClick = onSelectPlanClick, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.tertiary, contentColor = MaterialTheme.colorScheme.onTertiary, ), contentPadding = PaddingValues( vertical = 24.dp, horizontal = 40.dp, ), ) { Text(text = stringResource(R.string.label_select_plan_one)) Spacer(modifier = Modifier.width(12.dp)) Icon( painter = KenkoIcons.ArrowOutward, contentDescription = null, ) } } @Composable private fun ExploreExerciseCard( onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { HelperCards( onClick = onClick, onLongClick = onLongClick, modifier = modifier, ) { Text(text = stringResource(R.string.label_explore_exercises)) Icon( painter = KenkoIcons.ArrowOutward, contentDescription = null, modifier = Modifier .padding(16.dp) .align(TopEnd), ) } } @Composable private fun SessionHistoryCard( onClick: () -> Unit, modifier: Modifier = Modifier, ) { HelperCards(onClick = onClick, modifier = modifier) { Text(text = stringResource(R.string.label_session_history_home)) Icon( painter = KenkoIcons.History, contentDescription = null, modifier = Modifier .padding(16.dp) .align(TopEnd), ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun KenkoTopBar( modifier: Modifier = Modifier, actions: @Composable RowScope.() -> Unit = {}, ) { TopAppBar( title = { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Image( painter = painterResource(R.drawable.ic_app_icon), contentDescription = null, modifier = Modifier.clip(CircleShape) ) Text( text = "KENKO", fontWeight = FontWeight.Bold, ) } }, actions = actions, modifier = modifier, ) } @Composable private fun HelperCards( onClick: () -> Unit, modifier: Modifier = Modifier, onLongClick: () -> Unit = {}, shape: Shape = RectangleShape, color: Color = MaterialTheme.colorScheme.surface, textStyle: TextStyle = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), content: @Composable BoxScope.() -> Unit, ) { Surface( shape = shape, color = color, modifier = modifier.combinedClickable( onClick = onClick, onLongClick = onLongClick, ), ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { ProvideTextStyle(textStyle) { content() } } } } @Preview @Composable private fun HomePreview() { KenkoTheme { Home( state = HomeUiData( isPlanSelected = true, isSessionStarted = true, isTodayEmpty = false, isFirstSession = false, currentPlanId = null, ), ) } } @Preview @Composable private fun StartTodayPreview() { KenkoTheme { Home( state = HomeUiData( isPlanSelected = true, isSessionStarted = false, isTodayEmpty = false, isFirstSession = false, currentPlanId = null, ), ) } } @Preview @Composable private fun TodayEmptyPreview() { KenkoTheme { Home( state = HomeUiData( isPlanSelected = true, isSessionStarted = false, isTodayEmpty = true, isFirstSession = false, currentPlanId = null, ), ) } } @Preview @Composable private fun FirstStartHomePreview() { KenkoTheme { Home( state = HomeUiData( isPlanSelected = false, isSessionStarted = false, isTodayEmpty = false, isFirstSession = true, currentPlanId = null, ), ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/home/HomeViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.home import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import com.looker.kenko.data.model.localDate import com.looker.kenko.data.repository.PlanRepo import com.looker.kenko.data.repository.SessionRepo import com.looker.kenko.utils.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.combine @HiltViewModel class HomeViewModel @Inject constructor( planRepo: PlanRepo, sessionRepo: SessionRepo, ) : ViewModel() { private val planStream = planRepo.current private val sessionStream = sessionRepo.streamByDate(localDate) private val sessionsStream = sessionRepo.stream private val planItemStream = planRepo.planItems(localDate.dayOfWeek) val state = combine( planStream, sessionStream, sessionsStream, planItemStream, ) { currentPlan, currentSession, sessions, planItems -> val isFirstSession = sessions.size <= 1 && sessions.firstOrNull()?.date == localDate HomeUiData( isPlanSelected = currentPlan != null, isSessionStarted = currentSession != null && currentSession.sets.isNotEmpty(), isTodayEmpty = planItems.isEmpty(), isFirstSession = isFirstSession, currentPlanId = currentPlan?.id, ) }.asStateFlow( HomeUiData( isPlanSelected = true, isSessionStarted = false, isTodayEmpty = false, isFirstSession = false, currentPlanId = null, ), ) } @Immutable data class HomeUiData( val isPlanSelected: Boolean, val isSessionStarted: Boolean, val isTodayEmpty: Boolean, val isFirstSession: Boolean, val currentPlanId: Int?, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/home/navigation/HomeNavigation.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.home.navigation import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.looker.kenko.ui.home.Home import kotlinx.serialization.Serializable @Serializable object HomeRoute fun NavController.navigateToHome(navOptions: NavOptions? = null) { navigate(HomeRoute, navOptions = navOptions) } fun NavGraphBuilder.home( onSelectPlanClick: () -> Unit, onProfileClick: () -> Unit, onAddExerciseClick: () -> Unit, onExploreSessionsClick: () -> Unit, onExploreExercisesClick: () -> Unit, onStartSessionClick: () -> Unit, onCurrentPlanClick: (Int) -> Unit, ) { composable { Home( onProfileClick = onProfileClick, onSelectPlanClick = onSelectPlanClick, onAddExerciseClick = onAddExerciseClick, onExploreSessionsClick = onExploreSessionsClick, onExploreExercisesClick = onExploreExercisesClick, onStartSessionClick = onStartSessionClick, onCurrentPlanClick = onCurrentPlanClick, viewModel = hiltViewModel(), ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/navigation/KenkoNavHost.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.navigation import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.Lifecycle import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.navOptions import com.looker.kenko.ui.addEditExercise.navigation.addEditExercise import com.looker.kenko.ui.addEditExercise.navigation.navigateToAddEditExercise import com.looker.kenko.ui.exercises.navigation.exercises import com.looker.kenko.ui.exercises.navigation.navigateToExercises import com.looker.kenko.ui.getStarted.navigation.GetStartedRoute import com.looker.kenko.ui.getStarted.navigation.getStarted import com.looker.kenko.ui.home.navigation.home import com.looker.kenko.ui.home.navigation.navigateToHome import com.looker.kenko.ui.performance.navigation.performance import com.looker.kenko.ui.planEdit.navigation.navigateToPlanEdit import com.looker.kenko.ui.planEdit.navigation.planEdit import com.looker.kenko.ui.plans.navigation.navigateToPlans import com.looker.kenko.ui.plans.navigation.plans import com.looker.kenko.ui.profile.navigation.navigateToProfile import com.looker.kenko.ui.profile.navigation.profile import com.looker.kenko.ui.sessionDetail.navigation.navigateToSessionDetail import com.looker.kenko.ui.sessionDetail.navigation.sessionDetail import com.looker.kenko.ui.sessions.navigation.navigateToSessions import com.looker.kenko.ui.sessions.navigation.sessions import com.looker.kenko.ui.settings.navigation.navigateToSettings import com.looker.kenko.ui.settings.navigation.settings private val singleTopNavOptions = navOptions { launchSingleTop = true } private val splashNavOptions = navOptions { launchSingleTop = true popUpTo { inclusive = true } } @Composable fun KenkoNavHost( navController: NavController, modifier: Modifier = Modifier, startDestination: Any = GetStartedRoute, ) { NavHost( modifier = modifier, navController = navController as NavHostController, startDestination = startDestination, ) { getStarted { navController.navigateToHome(splashNavOptions) } home( onProfileClick = { navController.navigateToProfile(navOptions = singleTopNavOptions) }, onSelectPlanClick = { navController.navigateToPlans(navOptions = singleTopNavOptions) }, onAddExerciseClick = { navController.navigateToAddEditExercise(navOptions = singleTopNavOptions) }, onExploreSessionsClick = { navController.navigateToSessions(navOptions = singleTopNavOptions) }, onExploreExercisesClick = { navController.navigateToExercises(navOptions = singleTopNavOptions) }, onStartSessionClick = { navController.navigateToSessionDetail(date = null, navOptions = singleTopNavOptions) }, onCurrentPlanClick = { navController.navigateToPlanEdit(id = it, navOptions = singleTopNavOptions) }, ) sessions( onSessionClick = { date -> navController.navigateToSessionDetail( date = date, navOptions = singleTopNavOptions ) }, onBackPress = navController::popBackStackOnResume ) plans( onPlanClick = { navController.navigateToPlanEdit( id = it, navOptions = singleTopNavOptions ) }, onBackPress = navController::popBackStackOnResume ) settings(navController::popBackStackOnResume) profile( onAddExerciseClick = { navController.navigateToAddEditExercise(navOptions = singleTopNavOptions) }, onExercisesClick = { navController.navigateToExercises(navOptions = singleTopNavOptions) }, onPlanClick = { navController.navigateToPlans(navOptions = singleTopNavOptions) }, onSettingsClick = { navController.navigateToSettings(navOptions = singleTopNavOptions) }, onBackPress = navController::popBackStackOnResume, ) exercises( onExerciseClick = { id -> navController.navigateToAddEditExercise( id = id, navOptions = singleTopNavOptions ) }, onCreateClick = { target -> navController.navigateToAddEditExercise( target = target, navOptions = singleTopNavOptions ) }, onBackPress = navController::popBackStackOnResume ) planEdit(navController::popBackStackOnResume) { name, target -> navController.navigateToAddEditExercise( name = name, target = target, navOptions = singleTopNavOptions, ) } sessionDetail( onBackPress = navController::popBackStackOnResume, onHistoryClick = navController::navigateToSessionDetail, ) addEditExercise(navController::popBackStackOnResume) performance() } } private fun NavHostController.popBackStackOnResume() { if (lifecycleState?.isAtLeast(Lifecycle.State.RESUMED) == true) { popBackStack() } } private val NavHostController.lifecycleState: Lifecycle.State? get() = currentBackStackEntry?.lifecycle?.currentState ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/performance/Performance.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.performance import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeContentPadding import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.looker.kenko.ui.performance.components.Plot import com.looker.kenko.ui.performance.components.drawAxes import com.looker.kenko.ui.performance.components.drawGrid import com.looker.kenko.ui.performance.components.mapXY import com.looker.kenko.ui.performance.components.pathFor import com.looker.kenko.ui.performance.components.rememberPlot import com.looker.kenko.ui.theme.KenkoTheme @Composable fun Performance(viewModel: PerformanceViewModel) { val uiState by viewModel.state.collectAsStateWithLifecycle() Surface(Modifier.safeContentPadding()) { when (uiState) { is PerformanceUiState.Loading -> { Box( modifier = Modifier .fillMaxSize() .padding(bottom = 80.dp), contentAlignment = Alignment.Center, ) { CircularProgressIndicator() } } is PerformanceUiState.Success -> { val data = (uiState as PerformanceUiState.Success).data PerformancePlot( plot = rememberPlot(data.performance.ratings, data.performance.days), modifier = Modifier .fillMaxWidth() .height(300.dp), ) } is PerformanceStateError -> { val message = when (uiState) { PerformanceStateError.NoValidPerformance -> "No performance" PerformanceStateError.NotEnoughData -> "Not enough data" else -> error("Unknown") } Box( modifier = Modifier .fillMaxSize() .padding(bottom = 80.dp), contentAlignment = Alignment.Center, ) { Text(text = message, color = MaterialTheme.colorScheme.tertiary) } } } } } @Composable private fun PerformancePlot( plot: Plot, modifier: Modifier = Modifier, ) { val axesColor = MaterialTheme.colorScheme.outline val gridColor = MaterialTheme.colorScheme.surfaceVariant.copy(0.3F) Canvas(modifier) { val points = plot.mapXY(size) drawGrid(gridColor) drawAxes(axesColor) drawPath( path = pathFor(points), color = plot.lineColor, style = plot.style, ) points.forEach { drawCircle( center = it, color = plot.pointColor, radius = plot.pointRadius, ) } } } @Preview @Composable private fun PerformanceSmallPreview() { val ratings = floatArrayOf(8852.628F, 5092.6157F, 7432.0F) val days = intArrayOf(0, 1, 9) KenkoTheme { PerformancePlot( rememberPlot(ratings, days), modifier = Modifier .fillMaxWidth() .height(300.dp) .padding(8.dp), ) } } @Preview @Composable private fun PerformancePreview() { val ratings = floatArrayOf( 8852.628F, 3092.6155F, 2661.0654F, 11071.365F, 5737.648F, 4238.7886F, 11071.365F ) val days = intArrayOf(0, 1, 2, 3, 11, 15, 20) KenkoTheme { PerformancePlot( rememberPlot(ratings, days), modifier = Modifier .fillMaxWidth() .height(300.dp) .padding(8.dp), ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/performance/PerformanceViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.performance import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import com.looker.kenko.data.model.Plan import com.looker.kenko.data.repository.Performance import com.looker.kenko.data.repository.PerformanceRepo import com.looker.kenko.data.repository.PlanRepo import com.looker.kenko.utils.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @HiltViewModel class PerformanceViewModel @Inject constructor( private val repo: PerformanceRepo, planRepo: PlanRepo, ) : ViewModel() { val currentPlan = planRepo.current val state: StateFlow = currentPlan .map { plan -> if (plan == null) { PerformanceStateError.NoValidPerformance } else { val performance = repo.getPerformance(planId = plan.id) when { performance == null -> PerformanceStateError.NoValidPerformance performance.ratings.size < MinDataRequired -> PerformanceStateError.NotEnoughData else -> PerformanceUiState.Success( PerformanceUiData( plan = plan, performance = performance, ), ) } } } .onStart { emit(PerformanceUiState.Loading) } .asStateFlow(PerformanceUiState.Loading) } private const val MinDataRequired = 1 sealed interface PerformanceUiState { object Loading : PerformanceUiState class Success(val data: PerformanceUiData) : PerformanceUiState } sealed interface PerformanceStateError : PerformanceUiState { object NoValidPerformance : PerformanceStateError object NotEnoughData : PerformanceStateError } @Stable class PerformanceUiData( val plan: Plan?, val performance: Performance, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/performance/components/Axes.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.performance.components import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.DrawScope fun DrawScope.drawAxes( axesColor: Color, axisStroke: Float = 7F, strokeCap: StrokeCap = StrokeCap.Round, ) { val axisStrokePadding = axisStroke / 2 drawLine( color = axesColor, start = Offset(axisStrokePadding, axisStrokePadding), end = Offset(axisStrokePadding, size.height - axisStrokePadding), strokeWidth = axisStroke, cap = strokeCap, ) drawLine( color = axesColor, start = Offset(axisStrokePadding, size.height - axisStrokePadding), end = Offset( size.width - axisStrokePadding, size.height - axisStrokePadding ), strokeWidth = axisStroke, cap = strokeCap, ) } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/performance/components/Grid.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.performance.components import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope fun DrawScope.drawGrid( gridColor: Color, gridStroke: Float = 2F, verticalGridCount: Int = 4, horizontalGridCount: Int = 3, ) { if (verticalGridCount > 0) { val dx = size.width / (verticalGridCount + 1) for (i in 1..verticalGridCount) { val x = i * dx drawLine( color = gridColor, start = Offset(x, 0F), end = Offset(x, size.height), strokeWidth = gridStroke, ) } } if (horizontalGridCount > 0) { val dy = size.height / (horizontalGridCount + 1) for (j in 1..horizontalGridCount) { val y = j * dy drawLine( color = gridColor, start = Offset(0F, y), end = Offset(size.width, y), strokeWidth = gridStroke, ) } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/performance/components/Plot.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.performance.components import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.util.fastCoerceAtLeast import com.looker.kenko.utils.moveTo import kotlin.math.sign @Immutable class Plot( val ratings: FloatArray, val days: IntArray, val pointColor: Color, val pointRadius: Float, val lineColor: Color, val style: Stroke, ) fun pathFor(points: Array) = Path().apply { if (points.isEmpty()) return@apply val slope = FloatArray(points.size) moveTo(points[0]) // knowingly ignore the last index so the last tangent is horizontal for (i in 0.. { val pointCount = ratings.size val horizontalInset = size.width * horizontalPaddingPercentage val verticalInset = size.height * verticalPaddingPercentage val usableWidth = (size.width - horizontalInset - horizontalInset).fastCoerceAtLeast(0f) val usableHeight = (size.height - verticalInset - verticalInset).fastCoerceAtLeast(0f) var minRating = Float.POSITIVE_INFINITY var maxRating = Float.NEGATIVE_INFINITY for (i in 0.. maxRating) maxRating = rating } val minDay = days.first() val maxDay = days.last() return Array(pointCount) { i -> val rating = ratings[i] val normalized = (rating - minRating) / (maxRating - minRating) val y = verticalInset + (1f - normalized) * usableHeight val day = days[i] val normalizedDay = (day - minDay).toFloat() / (maxDay - minDay) val x = horizontalInset + normalizedDay * usableWidth Offset(x, y) } } @Composable fun rememberPlot( points: FloatArray, days: IntArray, pointColor: Color = MaterialTheme.colorScheme.primary, pointRadius: Float = 8F, lineColor: Color = MaterialTheme.colorScheme.primaryContainer, style: Stroke = PlotStyle, ): Plot = Plot( ratings = points, days = days, pointColor = pointColor, pointRadius = pointRadius, lineColor = lineColor, style = style, ) private val PlotStyle = Stroke( width = 8F, cap = StrokeCap.Round, join = StrokeJoin.Round, miter = 0F ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/performance/navigation/PerformanceNavigation.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.performance.navigation import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.looker.kenko.ui.performance.Performance import kotlinx.serialization.Serializable @Serializable object PerformanceRoute fun NavController.navigateToPerformance(navOptions: NavOptions? = null) { navigate(PerformanceRoute, navOptions) } fun NavGraphBuilder.performance() { composable { Performance(viewModel = hiltViewModel()) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/planEdit/PlanEdit.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.planEdit import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.SizeTransform import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.looker.kenko.BuildConfig import com.looker.kenko.R import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.ExercisesPreviewParameter import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.ui.components.BackButton import com.looker.kenko.ui.components.DaySelectorChip import com.looker.kenko.ui.components.ErrorSnackbar import com.looker.kenko.ui.components.HorizontalDaySelector import com.looker.kenko.ui.components.KenkoButton import com.looker.kenko.ui.extensions.normalizeInt import com.looker.kenko.ui.extensions.plus import com.looker.kenko.ui.planEdit.components.DaySwitcher import com.looker.kenko.ui.planEdit.components.ExerciseItem import com.looker.kenko.ui.planEdit.components.kenkoDayName import com.looker.kenko.ui.selectExercise.SelectExercise import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme import com.looker.kenko.ui.theme.numbers import com.looker.kenko.utils.minus import com.looker.kenko.utils.plus import kotlinx.coroutines.launch import kotlinx.datetime.DayOfWeek @Composable fun PlanEdit( viewModel: PlanEditViewModel, onBackPress: () -> Unit, onAddNewExerciseClick: (name: String?, target: MuscleGroups?) -> Unit, ) { val pageStage by viewModel.pageState.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle() BackHandler { viewModel.onBackPress(pageStage, onBackPress) } FullEdit( snackbarHostState = viewModel.snackbarState, stage = pageStage, fab = { PlanEditFAB( pageStage = pageStage, onClick = { if (pageStage == PlanEditStage.NameEdit) { viewModel.saveName() } else { viewModel.openSheet() } }, ) }, onBackPress = { viewModel.onBackPress(pageStage, onBackPress) }, onDebugMockClick = viewModel::debugFillMockData, ) { stage -> when (stage) { PlanEditStage.NameEdit -> { val isNameAlreadyUsed by viewModel.isNameAlreadyUsed.collectAsStateWithLifecycle() NameEdit( state = viewModel.planNameState, isNameAlreadyUsed = isNameAlreadyUsed, onSaveClick = viewModel::saveName, ) } PlanEditStage.PlanEdit -> { PlanEdit( state = state, onSelectDay = viewModel::setCurrentDay, onRemoveExerciseClick = viewModel::removeExercise, onFullDaySelection = viewModel::openFullDaySelection, ) } } } if (state.exerciseSheetVisible) { AddExerciseSheet( onDismiss = viewModel::closeSheet, onDone = viewModel::addExercise, onAddNewExerciseClick = onAddNewExerciseClick, ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun FullEdit( snackbarHostState: SnackbarHostState, stage: PlanEditStage, fab: @Composable () -> Unit, onBackPress: () -> Unit, onDebugMockClick: (() -> Unit)? = null, ui: @Composable (stage: PlanEditStage) -> Unit, ) { Scaffold( floatingActionButton = fab, snackbarHost = { SnackbarHost(hostState = snackbarHostState) { ErrorSnackbar(data = it) } }, floatingActionButtonPosition = FabPosition.Center, topBar = { TopAppBar( title = {}, navigationIcon = { BackButton(onBackPress) }, actions = { if (BuildConfig.DEBUG && stage == PlanEditStage.PlanEdit) { IconButton(onClick = { onDebugMockClick?.invoke() }) { Icon(painter = KenkoIcons.Add, contentDescription = "Mock data") } } }, ) }, ) { innerPadding -> AnimatedContent( modifier = Modifier.padding(innerPadding + PaddingValues(horizontal = 16.dp)), targetState = stage, label = "Plan edit stage", transitionSpec = { when (targetState) { PlanEditStage.NameEdit -> { slideInHorizontally { -it / 2 } + fadeIn() togetherWith slideOutHorizontally { it / 2 } + fadeOut() } PlanEditStage.PlanEdit -> { slideInHorizontally { it / 2 } + fadeIn() togetherWith slideOutHorizontally { -it / 2 } + fadeOut() } } using SizeTransform(clip = false) }, ) { ui(it) } } } @Composable private fun PlanEditFAB( pageStage: PlanEditStage, onClick: () -> Unit, modifier: Modifier = Modifier, ) { KenkoButton( modifier = modifier, onClick = onClick, label = { AnimatedContent( targetState = pageStage, label = "FAB label", transitionSpec = { when (targetState) { PlanEditStage.NameEdit -> { slideInVertically { it } + fadeIn() togetherWith slideOutVertically { -it } + fadeOut() } PlanEditStage.PlanEdit -> { slideInVertically { -it } + fadeIn() togetherWith slideOutVertically { it } + fadeOut() } } using SizeTransform(clip = false) }, ) { if (it == PlanEditStage.NameEdit) { Text(stringResource(R.string.label_next)) } else { Text(stringResource(R.string.label_add)) } } }, icon = { AnimatedContent( targetState = pageStage, label = "FAB icon", transitionSpec = { when (targetState) { PlanEditStage.NameEdit -> { slideInHorizontally { it * 2 } + fadeIn() togetherWith slideOutHorizontally { -it * 2 } + fadeOut() } PlanEditStage.PlanEdit -> { slideInHorizontally { -it * 2 } + fadeIn() togetherWith slideOutHorizontally { it * 2 } + fadeOut() } } using SizeTransform(clip = false) }, ) { if (it == PlanEditStage.NameEdit) { Icon( painter = KenkoIcons.ArrowForward, contentDescription = stringResource(R.string.label_next), ) } else { Icon( painter = KenkoIcons.Add, contentDescription = stringResource(R.string.label_add), ) } } }, ) } @Composable private fun NameEdit( state: TextFieldState, isNameAlreadyUsed: Boolean, onSaveClick: () -> Unit, modifier: Modifier = Modifier, ) { PlanName( planName = state, error = isNameAlreadyUsed, onNext = { onSaveClick() }, modifier = modifier.fillMaxSize(), ) } @Composable private fun PlanEdit( state: PlanEditState, onSelectDay: (DayOfWeek) -> Unit, onRemoveExerciseClick: (Exercise) -> Unit, onFullDaySelection: () -> Unit, ) { val focusManager = LocalFocusManager.current val isCurrentDayBlank by remember(state.exercises) { derivedStateOf { state.exercises.isEmpty() } } PlanExercise( modifier = Modifier.fillMaxSize(), header = { Header( isExpandedView = state.selectionMode, daySelector = { HorizontalDaySelector( item = { dayOfWeek -> DaySelectorChip( selected = dayOfWeek == state.currentDay, onClick = { onSelectDay(dayOfWeek) }, ) { Text(kenkoDayName(dayOfWeek)) } }, ) }, daySwitcher = { DaySwitcher( selected = state.currentDay, onNext = { onSelectDay(state.currentDay + 1) }, onPrevious = { onSelectDay(state.currentDay - 1) }, onClick = onFullDaySelection, ) }, ) }, items = { if (isCurrentDayBlank) { item { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { Text( text = stringResource(R.string.no_exercises_yet), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.error, ) } } } else { itemsIndexed(state.exercises) { index, exercise -> ExerciseItem( modifier = Modifier.animateItem(), exercise = exercise, ) { ExerciseItemActions( index = index, onRemove = { focusManager.clearFocus() onRemoveExerciseClick(exercise) }, ) } } } }, ) } @Composable private fun ExerciseItemActions( index: Int, onRemove: () -> Unit, ) { Row( verticalAlignment = Alignment.CenterVertically, ) { FilledTonalIconButton( onClick = onRemove, colors = IconButtonDefaults.filledTonalIconButtonColors( containerColor = MaterialTheme.colorScheme.errorContainer, ), ) { Icon(painter = KenkoIcons.Remove, contentDescription = null) } Spacer(modifier = Modifier.width(12.dp)) Text( text = normalizeInt(index + 1), style = LocalTextStyle.current.numbers(), ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AddExerciseSheet( onDismiss: () -> Unit, onDone: (Exercise) -> Unit, onAddNewExerciseClick: (name: String?, target: MuscleGroups?) -> Unit, ) { val scope = rememberCoroutineScope() val state = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet(sheetState = state, onDismissRequest = onDismiss) { SelectExercise( onRequestNewExercise = onAddNewExerciseClick, onDone = { exercise -> scope.launch { onDone(exercise) state.hide() }.invokeOnCompletion { if (!state.isVisible) onDismiss() } }, ) } } @Preview @Composable private fun ExerciseItemPreview( @PreviewParameter(ExercisesPreviewParameter::class, limit = 2) exercises: List, ) { KenkoTheme { ExerciseItem(exercise = exercises.first()) { ExerciseItemActions(index = 1) { } } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/planEdit/PlanEditViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.planEdit import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Stable import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.looker.kenko.R import com.looker.kenko.data.StringHandler import com.looker.kenko.data.local.model.SetType import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.PlanItem import com.looker.kenko.data.model.RepsInReserve import com.looker.kenko.data.model.localDate import com.looker.kenko.data.repository.PlanRepo import com.looker.kenko.ui.planEdit.navigation.PlanEditRoute import com.looker.kenko.utils.asStateFlow import com.looker.kenko.utils.nextLocalDateTime import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlin.concurrent.atomics.AtomicInt import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.concurrent.atomics.incrementAndFetch import kotlin.random.Random import kotlin.time.Clock import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.datetime.DayOfWeek @HiltViewModel class PlanEditViewModel @Inject constructor( private val repo: PlanRepo, private val stringHandler: StringHandler, private val sessionRepo: com.looker.kenko.data.repository.SessionRepo, savedStateHandle: SavedStateHandle, ) : ViewModel() { private val routeData: PlanEditRoute = savedStateHandle.toRoute() private val _planId: Int = routeData.id // if null show name edit else plan edit private val planIdStream = MutableStateFlow(_planId) val planNameState: TextFieldState = TextFieldState("") val snackbarState = SnackbarHostState() private val _isBackAlreadyPressedOnce = MutableStateFlow(false) @OptIn(ExperimentalCoroutinesApi::class) private val _planItemsStream = planIdStream.flatMapLatest { repo.planItems(it) } private val _dayOfWeek: MutableStateFlow = MutableStateFlow(localDate.dayOfWeek) private val _isSheetVisible: MutableStateFlow = MutableStateFlow(false) private val _fullDaySelection: MutableStateFlow = MutableStateFlow(false) @OptIn(FlowPreview::class) val isNameAlreadyUsed = snapshotFlow { planNameState.text.trim().toString() } .debounce(200.milliseconds) .map { repo.planNameExists(it) } .asStateFlow(false) val pageState: StateFlow = planIdStream.map { id -> if (id == -1) PlanEditStage.NameEdit else PlanEditStage.PlanEdit }.asStateFlow(PlanEditStage.NameEdit) val state: StateFlow = combine( _planItemsStream, _dayOfWeek, _fullDaySelection, _isSheetVisible, ) { items, day, daySelection, sheetVisible -> PlanEditState( currentDay = day, selectionMode = daySelection, exerciseSheetVisible = sheetVisible, exercises = items.filter { it.dayOfWeek == day }.map(PlanItem::exercise), ) }.asStateFlow( PlanEditState( currentDay = localDate.dayOfWeek, selectionMode = false, exerciseSheetVisible = false, exercises = emptyList(), ), ) fun saveName() { viewModelScope.launch { if (planNameState.text.isBlank()) { snackbarState.showSnackbar(stringHandler.getString(R.string.error_plan_name_empty)) return@launch } if (isNameAlreadyUsed.value) { snackbarState.showSnackbar(stringHandler.getString(R.string.error_plan_name_exists)) return@launch } val createId = repo.createPlan(planNameState.text.toString()) planIdStream.emit(createId) } } fun setCurrentDay(dayOfWeek: DayOfWeek) { viewModelScope.launch { _dayOfWeek.emit(dayOfWeek) if (_fullDaySelection.value) { _fullDaySelection.emit(false) } } } fun openFullDaySelection() { viewModelScope.launch { _fullDaySelection.emit(true) } } fun openSheet() { viewModelScope.launch { _isSheetVisible.emit(true) } } fun closeSheet() { viewModelScope.launch { _isSheetVisible.emit(false) } } fun addExercise(exercise: Exercise) { viewModelScope.launch { repo.addItem( PlanItem( dayOfWeek = _dayOfWeek.value, exercise = exercise, planId = planIdStream.value, ), ) } } fun removeExercise(exercise: Exercise) { viewModelScope.launch { repo.removeItemById(exercise.id!!) } } fun onBackPress(stage: PlanEditStage, onBackPress: () -> Unit) { viewModelScope.launch { if (stage == PlanEditStage.NameEdit) { onBackPress() return@launch } if (_isBackAlreadyPressedOnce.value) { repo.deletePlan(planIdStream.value) onBackPress() return@launch } if (repo.getPlanItems(planIdStream.value).isEmpty()) { _isBackAlreadyPressedOnce.emit(true) snackbarState.showSnackbar(stringHandler.getString(R.string.error_plan_empty_prompt)) return@launch } onBackPress() } } @OptIn(ExperimentalAtomicApi::class) fun debugFillMockData(sessions: Int = 9) { viewModelScope.launch { val now = Clock.System.now() val rand = Random.Default val added = AtomicInt(0) val planId = planIdStream.value var sessionsAdded = 0 while (sessionsAdded < sessions) { val date = rand.nextLocalDateTime(now - (sessions * 2).days, now).date val items = repo.getPlanItems(planId, date.dayOfWeek).ifEmpty { continue } sessionsAdded++ val sessionId = sessionRepo.getSessionIdOrCreate(date) for (item in items) { val exerciseId = item.exercise.id ?: continue val setsCount = rand.nextInt(1, 4) repeat(setsCount) { val weight = rand.nextInt(10, 80) + rand.nextFloat() val reps = rand.nextInt(5, 15) sessionRepo.addSet( sessionId = sessionId, exerciseId = exerciseId, weight = weight, reps = reps, setType = SetType.entries.random(), rir = RepsInReserve(2), ) added.incrementAndFetch() } } } snackbarState.showSnackbar("Mock data added: ${added.load()} sets") } } } @Stable enum class PlanEditStage { NameEdit, PlanEdit, } @Stable data class PlanEditState( val currentDay: DayOfWeek, val selectionMode: Boolean, val exerciseSheetVisible: Boolean, val exercises: List, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/planEdit/PlanExercise.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.planEdit import androidx.compose.animation.AnimatedContent import androidx.compose.animation.SizeTransform import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.looker.kenko.R @Composable fun PlanExercise( header: @Composable () -> Unit, items: LazyListScope.() -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), ) { LazyColumn( modifier = modifier, contentPadding = contentPadding, ) { stickyHeader { header() } items() } } @Composable fun Header( isExpandedView: Boolean, modifier: Modifier = Modifier, daySelector: @Composable () -> Unit, daySwitcher: @Composable () -> Unit, ) { Column { Text( text = stringResource(R.string.heading_select_plan_items), style = MaterialTheme.typography.displayMedium, color = MaterialTheme.colorScheme.secondary, ) Spacer(Modifier.height(8.dp)) AnimatedContent( modifier = modifier.background(MaterialTheme.colorScheme.surface), targetState = isExpandedView, label = "Header", transitionSpec = { if (targetState) { slideInVertically { it / 3 } + fadeIn() togetherWith slideOutVertically { -it / 3 } + fadeOut() } else { slideInVertically { -it / 3 } + fadeIn() togetherWith slideOutVertically { it / 3 } + fadeOut() } using SizeTransform(clip = false) }, contentAlignment = Alignment.Center, ) { expanded -> if (expanded) { daySelector() } else { daySwitcher() } } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/planEdit/PlanName.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.planEdit import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.KeyboardActionHandler import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.LineBreak import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.looker.kenko.R import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme @Composable fun PlanName( planName: TextFieldState, onNext: KeyboardActionHandler, modifier: Modifier = Modifier, error: Boolean = false, ) { Column( modifier = modifier, horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp), ) { Text( text = stringResource(R.string.heading_select_plan_name), style = MaterialTheme.typography.displayMedium, color = MaterialTheme.colorScheme.secondary, ) PlanNameField(state = planName, onNext = onNext) val contentColor by animateColorAsState( targetValue = if (error) { MaterialTheme.colorScheme.error } else { MaterialTheme.colorScheme.outline }, ) CompositionLocalProvider(LocalContentColor provides contentColor) { PlanNameSuggestion { if (error) { Text(text = stringResource(R.string.error_plan_name_exists)) } else { Text(text = stringResource(R.string.desc_select_plan_name)) } } } } } @Composable private fun PlanNameSuggestion( modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { Row( modifier = modifier.animateContentSize(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.Top, ) { Icon( painter = KenkoIcons.Info, contentDescription = null, ) ProvideTextStyle( value = MaterialTheme.typography.labelLarge.copy(lineBreak = LineBreak.Paragraph), content = content, ) } } @Composable private fun PlanNameField( state: TextFieldState, onNext: KeyboardActionHandler, modifier: Modifier = Modifier, ) { BasicTextField( state = state, modifier = modifier, textStyle = MaterialTheme.typography.titleLarge .merge(color = LocalContentColor.current), cursorBrush = SolidColor(LocalContentColor.current), keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.Words, imeAction = ImeAction.Next, ), onKeyboardAction = onNext, decorator = { Column( modifier = Modifier.padding(horizontal = 8.dp), ) { it() HorizontalDivider() } }, ) } @Preview @Composable private fun FieldPreview() { KenkoTheme { PlanName( planName = TextFieldState("Winter Arc"), error = false, onNext = {}, ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/planEdit/components/DaySwitcher.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.planEdit.components import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ContentTransform import androidx.compose.animation.SizeTransform import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.looker.kenko.R import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme import com.looker.kenko.utils.minus import com.looker.kenko.utils.plus import kotlinx.datetime.DayOfWeek import kotlinx.datetime.DayOfWeek.MONDAY import kotlinx.datetime.DayOfWeek.SUNDAY import kotlinx.datetime.DayOfWeek.THURSDAY @Composable fun DaySwitcher( selected: DayOfWeek, onNext: () -> Unit, onPrevious: () -> Unit, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier.widthIn(max = 400.dp), horizontalArrangement = Arrangement.Center, ) { val buttonColors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.surfaceVariant, contentColor = MaterialTheme.colorScheme.onSurfaceVariant, ) Button( modifier = Modifier.height(56.dp), colors = buttonColors, onClick = onPrevious, ) { Icon( painter = KenkoIcons.KeyboardArrowLeft, contentDescription = null, ) } Spacer(modifier = Modifier.width(6.dp)) Box( modifier = Modifier .height(56.dp) .weight(1F) .clip(MaterialTheme.shapes.large) .background(MaterialTheme.colorScheme.secondaryContainer) .clickable(onClick = onClick), contentAlignment = Alignment.Center, content = { AnimatedContent( targetState = selected, label = "", transitionSpec = { fadeAndSlideHorizontally( (initialState == SUNDAY && targetState == MONDAY) || (targetState > initialState && !(initialState == MONDAY && targetState == SUNDAY)), ) using SizeTransform(clip = false) }, ) { day -> Text( text = kenkoDayName(dayOfWeek = day), style = MaterialTheme.typography.labelLarge, ) } }, ) Spacer(modifier = Modifier.width(6.dp)) Button( modifier = Modifier.height(56.dp), colors = buttonColors, onClick = onNext, ) { Icon( painter = KenkoIcons.KeyboardArrowRight, contentDescription = null, ) } } } fun fadeAndSlideHorizontally(rightToLeft: Boolean = true): ContentTransform { return slideInHorizontally { it * if (rightToLeft) 1 else -1 } + fadeIn() togetherWith slideOutHorizontally { -it * if (rightToLeft) 1 else -1 } + fadeOut() } @Composable fun kenkoDayName(dayOfWeek: DayOfWeek): String { val kenkoNames = stringArrayResource(R.array.kenko_day_of_week) return remember(dayOfWeek) { kenkoNames[dayOfWeek.ordinal] } } @Composable fun dayName(dayOfWeek: DayOfWeek): String { val names = stringArrayResource(R.array.day_of_week) return remember(dayOfWeek) { names[dayOfWeek.ordinal] } } @Preview @Composable private fun DaySelectorPreview() { KenkoTheme { var isSelected by remember { mutableStateOf(THURSDAY) } DaySwitcher( onNext = { isSelected += 1 }, onPrevious = { isSelected -= 1 }, onClick = { }, selected = isSelected, ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/planEdit/components/ExerciseItem.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.planEdit.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.looker.kenko.R import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.ExercisesPreviewParameter import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme @Composable fun ExerciseItem( exercise: Exercise, modifier: Modifier = Modifier, onClick: () -> Unit = {}, content: @Composable () -> Unit = {}, ) { Surface( modifier = modifier, color = Color.Transparent, onClick = onClick, shape = MaterialTheme.shapes.large, ) { Row( modifier = Modifier .fillMaxWidth() .heightIn(24.dp) .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { Column( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.SpaceAround, ) { Text( text = exercise.name, maxLines = 2, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, ) Text( text = stringResource(exercise.target.stringRes), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline, ) } CompositionLocalProvider( LocalTextStyle provides MaterialTheme.typography.headlineSmall, LocalContentColor provides MaterialTheme.colorScheme.primary, content = content, ) } } } @Composable fun KenkoAddButton(onClick: () -> Unit) { Button( onClick = onClick, contentPadding = PaddingValues( top = 20.dp, bottom = 20.dp, start = 32.dp, end = 40.dp, ), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer, ), ) { Icon(painter = KenkoIcons.Add, contentDescription = null) Spacer(modifier = Modifier.width(8.dp)) Text(text = stringResource(R.string.label_add)) } } @Preview(showBackground = true) @Composable private fun ExerciseItemPreview( @PreviewParameter(ExercisesPreviewParameter::class, limit = 2) exercises: List, ) { KenkoTheme { ExerciseItem(exercise = exercises.first()) { Text(text = "01") } } } @PreviewLightDark @Composable private fun ExerciseButtonPreview() { KenkoTheme { KenkoAddButton { } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/planEdit/navigation/PlanEditNavigation.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.planEdit.navigation import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.ui.planEdit.PlanEdit import kotlinx.serialization.Serializable @Serializable data class PlanEditRoute( val id: Int, ) fun NavController.navigateToPlanEdit(id: Int = -1, navOptions: NavOptions? = null) { navigate(PlanEditRoute(id), navOptions) } fun NavGraphBuilder.planEdit( onBackPress: () -> Unit, onAddNewExerciseClick: (name: String?, target: MuscleGroups?) -> Unit, ) { composable { PlanEdit( onBackPress = onBackPress, onAddNewExerciseClick = onAddNewExerciseClick, viewModel = hiltViewModel() ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/plans/Plan.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.plans import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.looker.kenko.R import com.looker.kenko.data.model.Plan import com.looker.kenko.data.model.PlanPreviewParameters import com.looker.kenko.ui.components.BackButton import com.looker.kenko.ui.components.KenkoBorderWidth import com.looker.kenko.ui.components.SwipeToDeleteBox import com.looker.kenko.ui.components.endItem import com.looker.kenko.ui.planEdit.components.KenkoAddButton import com.looker.kenko.ui.plans.components.PlanItem import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme @Composable fun Plan( viewModel: PlanViewModel, onBackPress: () -> Unit, onPlanClick: (Int) -> Unit, ) { var showHelpDialog by remember { mutableStateOf(false) } val plans: List by viewModel.plans.collectAsStateWithLifecycle() Plan( plans = plans, onBackPress = onBackPress, onInfoClick = { showHelpDialog = true }, onSelectPlan = viewModel::switchPlan, onRemove = viewModel::removePlan, onPlanClick = onPlanClick, ) if (showHelpDialog) { AlertDialog( onDismissRequest = { showHelpDialog = false }, title = { Text(text = stringResource(R.string.label_clean_up)) }, text = { Text(text = stringResource(R.string.label_clean_up_plans)) }, confirmButton = { Button( onClick = { viewModel.cleanupPlans { showHelpDialog = false } }, ) { Text(text = stringResource(R.string.label_yes)) } }, dismissButton = { TextButton(onClick = { showHelpDialog = false }) { Text(text = stringResource(R.string.label_no)) } } ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun Plan( plans: List, onBackPress: () -> Unit, onInfoClick: () -> Unit, onSelectPlan: (Plan) -> Unit, onRemove: (Int) -> Unit, onPlanClick: (Int) -> Unit, ) { Scaffold( topBar = { TopAppBar( navigationIcon = { BackButton(onClick = onBackPress) }, title = { Text(text = stringResource(R.string.label_plans_title)) }, actions = { IconButton(onClick = onInfoClick) { Icon( painter = KenkoIcons.Info, contentDescription = "Info" ) } } ) }, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { KenkoAddButton(onClick = { onPlanClick(-1) }) }, containerColor = MaterialTheme.colorScheme.surface, ) { LazyColumn( contentPadding = it, verticalArrangement = Arrangement.spacedBy(1.dp), ) { items( items = plans, key = { plan -> plan.id!! }, ) { plan -> val isLast = remember(plan) { plans.last() == plan } SwipeToDeleteBox( modifier = Modifier.animateItem(), onDismiss = { onRemove(plan.id!!) }, ) { PlanItem( plan = plan, onClick = { onPlanClick(plan.id!!) }, onActiveChange = { onSelectPlan(plan) }, ) } if (!isLast) HorizontalDivider(thickness = KenkoBorderWidth) } endItem() } } } @Preview @Composable private fun PlanPreview( @PreviewParameter(PlanPreviewParameters::class) plans: List, ) { KenkoTheme { Plan( plans = plans, onSelectPlan = {}, onInfoClick = {}, onBackPress = {}, onPlanClick = {}, onRemove = {}, ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/plans/PlanViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.plans import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.looker.kenko.data.model.Plan import com.looker.kenko.data.repository.PlanRepo import com.looker.kenko.data.repository.SettingsRepo import com.looker.kenko.utils.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @HiltViewModel class PlanViewModel @Inject constructor( private val repo: PlanRepo, private val settingsRepo: SettingsRepo, ) : ViewModel() { val plans = repo.plans.asStateFlow(emptyList()) fun removePlan(id: Int) { viewModelScope.launch { repo.deletePlan(id) } } fun switchPlan(plan: Plan) { viewModelScope.launch { if (!plan.isActive) { repo.setCurrent(plan.id!!) } else { repo.updatePlan(plan.copy(isActive = false)) } if (repo.current.first() != null) { settingsRepo.setOnboardingDone() } } } fun cleanupPlans(onDone: () -> Unit) { viewModelScope.launch { repo.deleteEmptyPlans() onDone() } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/plans/components/PlanItem.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.plans.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColor import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedIconToggleButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.looker.kenko.R import com.looker.kenko.data.model.Plan import com.looker.kenko.data.model.PlanPreviewParameters import com.looker.kenko.ui.extensions.normalizeInt import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme @Composable fun PlanItem( plan: Plan, onActiveChange: (Boolean) -> Unit, onClick: () -> Unit, modifier: Modifier = Modifier, ) { val transition = updateTransition(targetState = plan.isActive, label = null) val background by transition.animateColor(label = "background") { if (it) { MaterialTheme.colorScheme.secondaryContainer } else { MaterialTheme.colorScheme.surfaceContainer } } val contentColor by transition.animateColor(label = "foreground") { if (it) { MaterialTheme.colorScheme.onSecondaryContainer } else { MaterialTheme.colorScheme.onSurface } } Surface( onClick = onClick, color = background, contentColor = contentColor, ) { Column( modifier = modifier.padding(16.dp), ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { Text( modifier = Modifier.weight(1F), text = plan.name, maxLines = 2, style = MaterialTheme.typography.headlineMedium, ) OutlinedIconToggleButton( checked = plan.isActive, onCheckedChange = onActiveChange, ) { Icon(painter = KenkoIcons.Done, contentDescription = null) } } AnimatedVisibility(visible = plan.isActive) { Text( text = ".selected", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline, ) } if (plan.isActive) { Spacer(modifier = Modifier.height(4.dp)) } val stats = remember(plan) { plan.stat } Text( text = stringResource( R.string.label_plan_description, stats.exercises, normalizeInt(stats.workDays), normalizeInt(7 - stats.workDays), ), ) } } } @PreviewLightDark @Composable private fun PlanItemPreview(@PreviewParameter(PlanPreviewParameters::class) plans: List) { KenkoTheme { var plan by remember { mutableStateOf(plans.first()) } PlanItem(plan = plan, { plan = plan.copy(isActive = it) }, {}) } } @PreviewLightDark @Composable private fun PlanItemInActivePreview(@PreviewParameter(PlanPreviewParameters::class) plans: List) { KenkoTheme { PlanItem(plan = plans.first(), {}, {}) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/plans/navigation/PlanNavigation.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.plans.navigation import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.looker.kenko.ui.plans.Plan import kotlinx.serialization.Serializable @Serializable object PlanRoute fun NavController.navigateToPlans(navOptions: NavOptions? = null) { navigate(PlanRoute, navOptions) } fun NavGraphBuilder.plans( onPlanClick: (Int) -> Unit, onBackPress: () -> Unit, ) { composable { Plan( onPlanClick = onPlanClick, onBackPress = onBackPress, viewModel = hiltViewModel(), ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/profile/Profile.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.profile import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.paddingFromBaseline import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledIconButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.looker.kenko.R import com.looker.kenko.data.model.PlanStat import com.looker.kenko.ui.components.BackButton import com.looker.kenko.ui.components.HealthQuotes import com.looker.kenko.ui.components.KenkoBorderWidth import com.looker.kenko.ui.components.OutlineBorder import com.looker.kenko.ui.components.SecondaryBorder import com.looker.kenko.ui.extensions.PHI import com.looker.kenko.ui.extensions.normalizeInt import com.looker.kenko.ui.extensions.plus import com.looker.kenko.ui.extensions.vertical import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme import com.looker.kenko.ui.theme.end import com.looker.kenko.ui.theme.numbers import com.looker.kenko.ui.theme.start @Composable fun Profile( viewModel: ProfileViewModel, onBackPress: () -> Unit, onExercisesClick: () -> Unit, onAddExerciseClick: () -> Unit, onPlanClick: () -> Unit, onSettingsClick: () -> Unit, ) { val state by viewModel.state.collectAsStateWithLifecycle() Profile( state = state, onBackPress = onBackPress, onSettingsClick = onSettingsClick, onPlanClick = onPlanClick, onAddExerciseClick = onAddExerciseClick, onExercisesClick = onExercisesClick, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun Profile( state: ProfileUiState, onBackPress: () -> Unit, onSettingsClick: () -> Unit, onPlanClick: () -> Unit, onAddExerciseClick: () -> Unit, onExercisesClick: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( topBar = { TopAppBar( title = { Text(text = stringResource(R.string.label_profile)) }, navigationIcon = { BackButton(onBackPress) }, actions = { IconButton(onClick = onSettingsClick) { Icon(painter = KenkoIcons.Settings, contentDescription = null) } }, ) }, containerColor = MaterialTheme.colorScheme.surface, ) { innerPadding -> Column( modifier = modifier .fillMaxSize() .padding(innerPadding + PaddingValues(horizontal = 16.dp)) .verticalScroll(rememberScrollState()), ) { if (state.isPlanAvailable) { CurrentPlanCard( onPlanClick = onPlanClick, name = state.planName, content = { Text( text = stringResource( R.string.label_plan_description, state.planStat!!.exercises, normalizeInt(state.planStat.workDays), normalizeInt(state.planStat.restDays), ), ) }, ) } else { SelectPlanCard(onPlanClick) } Spacer(modifier = Modifier.height(12.dp)) ExerciseCard( numberOfExercises = state.numberOfExercises, onAddClick = onAddExerciseClick, onExercisesClick = onExercisesClick, ) if (state.totalLifts > 0) { Spacer(modifier = Modifier.height(12.dp)) LiftsCard(state.totalLifts) } Spacer(modifier = Modifier.weight(1F)) HealthQuotes(Modifier.align(Alignment.CenterHorizontally)) } } } @Composable private fun CurrentPlanCard( onPlanClick: () -> Unit, name: String, content: @Composable ColumnScope.() -> Unit, modifier: Modifier = Modifier, ) { Surface( modifier = modifier .fillMaxWidth() .aspectRatio(PHI), color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = MaterialTheme.shapes.extraLarge, border = SecondaryBorder, onClick = onPlanClick, ) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceEvenly, ) { Row( modifier = Modifier .fillMaxWidth() .padding(start = 24.dp, top = 2.dp, end = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon(painter = KenkoIcons.Plan, contentDescription = null) Spacer(modifier = Modifier.width(12.dp)) Text( text = stringResource(R.string.label_current_plan), style = MaterialTheme.typography.titleMedium, ) Spacer(modifier = Modifier.weight(1F)) FilledIconButton(onClick = onPlanClick) { Icon(painter = KenkoIcons.Rename, contentDescription = null) } } HorizontalDivider( thickness = KenkoBorderWidth, color = MaterialTheme.colorScheme.secondary, ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Column( modifier = Modifier .padding(start = 24.dp, bottom = 16.dp), ) { Text( text = name, style = MaterialTheme.typography.titleLarge, ) Spacer(modifier = Modifier.height(8.dp)) CompositionLocalProvider( LocalTextStyle provides MaterialTheme.typography.bodyLarge, LocalContentColor provides MaterialTheme.colorScheme.outline, ) { content() } } Icon( imageVector = KenkoIcons.Stack, contentDescription = null, tint = MaterialTheme.colorScheme.secondary, modifier = Modifier.offset(x = 0.dp), ) } } } } @Composable fun SelectPlanCard( onSelectPlanClick: () -> Unit, modifier: Modifier = Modifier, ) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onTertiaryContainer) { Row( modifier = modifier .fillMaxWidth() .wrapContentHeight() .clip(CircleShape) .background(MaterialTheme.colorScheme.tertiaryContainer) .clickable(onClick = onSelectPlanClick) .padding(vertical = 24.dp), horizontalArrangement = Arrangement.SpaceAround, verticalAlignment = Alignment.CenterVertically, ) { Text( text = stringResource(R.string.label_select_plan), style = MaterialTheme.typography.headlineLarge, modifier = Modifier.paddingFromBaseline(bottom = 16.dp) ) Icon( painter = KenkoIcons.ArrowOutward, contentDescription = null, ) } } } @Composable private fun ExerciseCard( numberOfExercises: Int, onAddClick: () -> Unit, onExercisesClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier.height(IntrinsicSize.Max), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { val cardShape = MaterialTheme.shapes.extraLarge val surfaceShape = remember(cardShape) { cardShape.end(16.dp, 16.dp) } Surface( modifier = Modifier.weight(1.5F), shape = surfaceShape, border = OutlineBorder, onClick = onExercisesClick, ) { Column(Modifier.padding(24.dp)) { Text( text = stringResource(R.string.label_exercise), style = MaterialTheme.typography.titleMedium, ) Text( text = numberOfExercises.toString(), style = MaterialTheme.typography.headlineLarge, ) } } val buttonShape = remember(cardShape) { cardShape.start(16.dp, 16.dp) } Box( modifier = Modifier .weight(1F) .fillMaxHeight() .clip(buttonShape) .clickable(onClick = onAddClick) .border( border = SecondaryBorder, shape = buttonShape, ) .background(MaterialTheme.colorScheme.secondaryContainer), contentAlignment = Alignment.Center, ) { Icon( painter = KenkoIcons.Add, tint = MaterialTheme.colorScheme.onSecondaryContainer, contentDescription = stringResource(R.string.label_add), ) } } } @Composable private fun LiftsCard(setsPerformed: Int) { Surface( modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.extraLarge, border = SecondaryBorder, ) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( modifier = Modifier.vertical(false), text = stringResource(R.string.label_lifts), style = MaterialTheme.typography.titleMedium, ) Spacer(modifier = Modifier.width(12.dp)) Text( text = setsPerformed.toString(), style = MaterialTheme.typography.displayLarge.numbers(), ) Spacer(modifier = Modifier.weight(1F)) Icon( imageVector = KenkoIcons.Reveal, tint = MaterialTheme.colorScheme.surfaceContainerHigh, contentDescription = null, modifier = Modifier.offset(x = 30.dp), ) } } } @Preview(showBackground = true) @Composable private fun PlanCard() { KenkoTheme { CurrentPlanCard( onPlanClick = { }, name = "Push-Pull-Leg", content = { Text( text = stringResource( R.string.label_plan_description, 12, normalizeInt(5), normalizeInt(2), ), ) }, ) } } @Preview(showBackground = true) @Composable private fun EmptyPlanCardPreview() { KenkoTheme { SelectPlanCard({}) } } @Preview(showBackground = true) @Composable private fun ExerciseCardPreview() { KenkoTheme { ExerciseCard(21, {}, {}) } } @Preview @Composable private fun ProfileNoPlanPreview() { KenkoTheme { Profile( state = ProfileUiState(12, false, "Push-Pull-Leg", 2, PlanStat(12, 5)), onBackPress = { }, onSettingsClick = { }, onPlanClick = { }, onAddExerciseClick = { }, onExercisesClick = { }, ) } } @Preview @Composable private fun ProfilePreview() { KenkoTheme { Profile( state = ProfileUiState(12, true, "Push-Pull-Leg", 2, PlanStat(12, 5)), onBackPress = { }, onSettingsClick = { }, onPlanClick = { }, onAddExerciseClick = { }, onExercisesClick = { }, ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/profile/ProfileViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.profile import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import com.looker.kenko.data.model.Plan import com.looker.kenko.data.model.PlanStat import com.looker.kenko.data.repository.ExerciseRepo import com.looker.kenko.data.repository.PlanRepo import com.looker.kenko.data.repository.SessionRepo import com.looker.kenko.utils.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @HiltViewModel class ProfileViewModel @Inject constructor( planRepo: PlanRepo, sessionRepo: SessionRepo, exerciseRepo: ExerciseRepo, ) : ViewModel() { private val currentPlan: Flow = planRepo.current val state: StateFlow = combine( currentPlan, sessionRepo.setsCount, exerciseRepo.numberOfExercise, ) { plan, sets, number -> ProfileUiState( numberOfExercises = number, totalLifts = sets, isPlanAvailable = plan != null, planName = plan?.name ?: "", planStat = plan?.stat, ) }.asStateFlow(ProfileUiState()) } @Stable data class ProfileUiState( val numberOfExercises: Int = 0, val isPlanAvailable: Boolean = false, val planName: String = "", val totalLifts: Int = 0, val planStat: PlanStat? = null, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/profile/navigation/ProfileNavigation.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.profile.navigation import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.looker.kenko.ui.profile.Profile import kotlinx.serialization.Serializable @Serializable object ProfileRoute fun NavController.navigateToProfile(navOptions: NavOptions? = null) { navigate(ProfileRoute, navOptions) } fun NavGraphBuilder.profile( onBackPress: () -> Unit, onExercisesClick: () -> Unit, onAddExerciseClick: () -> Unit, onPlanClick: () -> Unit, onSettingsClick: () -> Unit, ) { composable { Profile( onBackPress = onBackPress, onExercisesClick = onExercisesClick, onAddExerciseClick = onAddExerciseClick, onPlanClick = onPlanClick, onSettingsClick = onSettingsClick, viewModel = hiltViewModel(), ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/selectExercise/SelectExercise.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.selectExercise import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.looker.kenko.R import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.ui.components.LazyTargets import com.looker.kenko.ui.components.TargetChip import com.looker.kenko.ui.components.disableScrollConnection import com.looker.kenko.ui.components.kenkoTextFieldColor import com.looker.kenko.ui.exercises.string import com.looker.kenko.ui.planEdit.components.ExerciseItem import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme import com.looker.kenko.ui.theme.end import com.looker.kenko.ui.theme.start @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SelectExercise( onDone: (Exercise) -> Unit, onRequestNewExercise: (name: String?, target: MuscleGroups?) -> Unit, ) { val viewModel: SelectExerciseViewModel = hiltViewModel() Column( modifier = Modifier .nestedScroll(disableScrollConnection()) .wrapContentHeight(), ) { val target by viewModel.targetMuscle.collectAsStateWithLifecycle() val searchResult by viewModel.searchResult.collectAsStateWithLifecycle() AddExerciseHeader(modifier = Modifier.padding(horizontal = 16.dp)) ExerciseSearchField( modifier = Modifier.padding(horizontal = 16.dp), name = viewModel.searchQuery, onNameChange = viewModel::setSearch, onAddClick = { onRequestNewExercise(viewModel.searchQuery.ifBlank { null }, target) }, ) LazyTargets(contentPadding = PaddingValues(horizontal = 8.dp)) { TargetChip( selected = target == it, onClick = { viewModel.setTarget(it) }, text = stringResource(it.string), ) } Box( contentAlignment = Alignment.Center, modifier = Modifier.height(240.dp), ) { when (searchResult) { SearchResult.Loading -> ContainedLoadingIndicator() SearchResult.NotFound -> SearchNotFound( onAddNewExercise = { onRequestNewExercise(viewModel.searchQuery, target) } ) is SearchResult.Success -> SearchResult( searchResult = searchResult as SearchResult.Success, onClick = onDone ) } } } } @Composable private fun SearchResult( searchResult: SearchResult.Success, onClick: (Exercise) -> Unit, ) { LazyColumn(Modifier.fillMaxSize()) { items(searchResult.exercises) { exercise -> ExerciseItem( exercise = exercise, onClick = { onClick(exercise) }, ) } } } @Composable private fun SearchNotFound(onAddNewExercise: () -> Unit, modifier: Modifier = Modifier) { Box( contentAlignment = Alignment.Center, modifier = modifier .fillMaxSize() .padding(horizontal = 12.dp) .background( color = MaterialTheme.colorScheme.errorContainer, shape = MaterialTheme.shapes.large ) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( text = stringResource(R.string.error_cant_find_exercise), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onErrorContainer, ) Spacer(modifier = Modifier.height(12.dp)) Button( onClick = onAddNewExercise, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.onErrorContainer, contentColor = MaterialTheme.colorScheme.errorContainer ), ) { Icon(painter = KenkoIcons.Add, contentDescription = null) Spacer(modifier = Modifier.width(4.dp)) Text(text = stringResource(R.string.label_create_exercise)) } } } } @Composable private fun ExerciseSearchField( name: String, onNameChange: (String) -> Unit, onAddClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, ) { TextField( modifier = Modifier.weight(1f), value = name, onValueChange = onNameChange, colors = kenkoTextFieldColor(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), shape = MaterialTheme.shapes.large.end(8.dp), label = { Text(text = stringResource(R.string.label_search_exercise)) }, ) Spacer(modifier = Modifier.width(8.dp)) FilledTonalIconButton( modifier = Modifier.size(56.dp), shape = MaterialTheme.shapes.large.start(8.dp), onClick = onAddClick, ) { Icon(painter = KenkoIcons.Add, contentDescription = null) } } } @Composable private fun AddExerciseHeader( modifier: Modifier = Modifier, ) { Text( modifier = modifier, text = stringResource(R.string.label_add_exercise_header), style = MaterialTheme.typography.displayMedium, color = MaterialTheme.colorScheme.tertiary ) } @Preview @Composable private fun ErrorPreview() { KenkoTheme { SearchNotFound(onAddNewExercise = {}) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/selectExercise/SelectExerciseViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.selectExercise import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.MuscleGroups import com.looker.kenko.data.repository.ExerciseRepo import com.looker.kenko.utils.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SelectExerciseViewModel @Inject constructor( repo: ExerciseRepo, ) : ViewModel() { var searchQuery: String by mutableStateOf("") private set private val searchQueryFlow = snapshotFlow { searchQuery } private val exerciseStream = repo.stream private val _targetMuscle: MutableStateFlow = MutableStateFlow(null) val targetMuscle: StateFlow = _targetMuscle.asStateFlow() val searchResult = combine( searchQueryFlow, targetMuscle, exerciseStream, ) { query, target, exercises -> val filteredExercises = exercises .filter { (it.target == target || target == null) && it.satisfiesSearch(query) } if (filteredExercises.isNotEmpty()) { SearchResult.Success(filteredExercises) } else { SearchResult.NotFound } }.asStateFlow(SearchResult.Loading) fun setTarget(target: MuscleGroups?) { viewModelScope.launch { _targetMuscle.emit(target) } } fun setSearch(value: String) { searchQuery = value } private fun Exercise.satisfiesSearch(query: String): Boolean { return query.isBlank() || name.contains(query, ignoreCase = true) } } @Stable sealed interface SearchResult { @Stable data object Loading : SearchResult @Stable data class Success(val exercises: List) : SearchResult @Stable data object NotFound : SearchResult } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/sessionDetail/SessionDetail.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.sessionDetail import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonShapes import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.Set import com.looker.kenko.ui.addSet.AddSet import com.looker.kenko.ui.components.BackButton import com.looker.kenko.ui.components.SwipeToDeleteBox import com.looker.kenko.ui.components.TypingText import com.looker.kenko.ui.extensions.normalizeInt import com.looker.kenko.ui.extensions.plus import com.looker.kenko.ui.planEdit.components.dayName import com.looker.kenko.ui.sessionDetail.components.SetItem import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme import com.looker.kenko.utils.DateFormat import com.looker.kenko.utils.formatDate import java.util.* import kotlin.time.Clock import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Instant import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate @Composable fun SessionDetails( viewModel: SessionDetailViewModel, onBackPress: () -> Unit, onHistoryClick: (LocalDate) -> Unit, ) { val state by viewModel.state.collectAsStateWithLifecycle() SessionDetail( state = state, onBackPress = onBackPress, onTimerClick = viewModel::resetRestTimer, onRemoveSet = viewModel::removeSet, onReferenceClick = viewModel::openReference, onSelectBottomSheet = viewModel::showBottomSheet, onHistoryClick = { onHistoryClick(viewModel.previousSessionDate) }, ) val exercise by viewModel.current.collectAsStateWithLifecycle() if (exercise != null) { AddSetSheet( exercise = exercise!!, onDismiss = viewModel::hideSheet, onAddSet = viewModel::startRestTimer, ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SessionDetail( state: SessionDetailState, onBackPress: () -> Unit = {}, onTimerClick: () -> Unit = {}, onRemoveSet: (Int?) -> Unit = {}, onReferenceClick: (String) -> Unit = {}, onSelectBottomSheet: (Exercise) -> Unit = {}, onHistoryClick: () -> Unit = {}, ) { when (state) { is SessionDetailState.Error -> { Column(Modifier.statusBarsPadding()) { TopAppBar( navigationIcon = { BackButton(onClick = onBackPress) }, title = {}, ) SessionError( title = stringResource(state.title), message = stringResource(state.errorMessage), modifier = Modifier.fillMaxSize(), ) } } SessionDetailState.Loading -> { Column(Modifier.statusBarsPadding()) { TopAppBar( navigationIcon = { BackButton(onClick = onBackPress) }, title = {}, ) Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { CircularProgressIndicator() } } } is SessionDetailState.Success -> { val data = state.data SetsList( date = data.date, exerciseSets = data.sets, lastSetTime = data.lastSetTime, isEditable = data.isToday, hasPreviousSession = data.hasPreviousSession, onBackPress = onBackPress, onTimerClick = onTimerClick, onRemoveSet = onRemoveSet, onReferenceClick = onReferenceClick, onSelectBottomSheet = onSelectBottomSheet, onHistoryClick = onHistoryClick, ) } } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun SetsList( date: LocalDate, exerciseSets: Map>, lastSetTime: Instant?, isEditable: Boolean, hasPreviousSession: Boolean, onBackPress: () -> Unit, onTimerClick: () -> Unit, onRemoveSet: (Int?) -> Unit, onReferenceClick: (String) -> Unit, onSelectBottomSheet: (Exercise) -> Unit, onHistoryClick: () -> Unit, ) { LazyVerticalGrid( columns = GridCells.Adaptive(360.dp), horizontalArrangement = Arrangement.SpaceEvenly, contentPadding = WindowInsets.navigationBars.asPaddingValues(LocalDensity.current) + PaddingValues(bottom = 12.dp), ) { item( span = { GridItemSpan(maxLineSpan) }, ) { Header( performedOn = date, onBackPress = onBackPress, actions = { if (hasPreviousSession) { IconButton(onClick = onHistoryClick) { Icon( painter = KenkoIcons.History, contentDescription = null, ) } } if (lastSetTime != null && lastSetTime - Clock.System.now() < 1.hours) { TimerBox( lastSetTime = lastSetTime, onTimerClick = onTimerClick ) } }, ) } exerciseSets.forEach { (exercise, sets) -> item( span = { GridItemSpan(maxLineSpan) }, ) { StickyHeader(name = exercise.name) { if (!exercise.reference.isNullOrBlank()) { FilledTonalIconButton(onClick = { onReferenceClick(exercise.reference) }) { Icon(painter = KenkoIcons.Lightbulb, contentDescription = null) } } if (isEditable) { FilledTonalIconButton( shapes = IconButtonShapes( shape = MaterialShapes.Circle.toShape(), pressedShape = MaterialShapes.Cookie6Sided.toShape(), ), onClick = { onSelectBottomSheet(exercise) }, ) { Icon(painter = KenkoIcons.Add, contentDescription = null) } } } } itemsIndexed(items = sets) { index, set -> SwipeToDeleteBox( modifier = Modifier.animateItem(), onDismiss = { onRemoveSet(set.id) }, ) { SetItem( modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp), set = set, title = { Text(normalizeInt(index + 1)) }, ) } } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun Header( performedOn: LocalDate, onBackPress: () -> Unit, modifier: Modifier = Modifier, actions: @Composable (RowScope.() -> Unit), ) { val date = remember { formatDate(performedOn, DateFormat.SessionLabel) } TopAppBar( modifier = modifier, actions = actions, navigationIcon = { BackButton(onClick = onBackPress) }, title = { Column( verticalArrangement = Arrangement.Center, ) { var startAnimatingDate by remember { mutableStateOf(false) } TypingText( text = dayName(performedOn.dayOfWeek), onCompleteListener = { startAnimatingDate = true }, ) TypingText( text = date, startTyping = startAnimatingDate, initialDelay = 0.milliseconds, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.outline, ) } }, ) } @Composable fun TimerBox( lastSetTime: Instant, onTimerClick: () -> Unit, ) { var restTimeInSeconds by remember { mutableLongStateOf(0L) } LaunchedEffect(Unit) { while (true) { restTimeInSeconds = (Clock.System.now() - lastSetTime).inWholeSeconds delay(1000) } } Surface( color = MaterialTheme.colorScheme.secondaryContainer, onClick = onTimerClick, shape = CircleShape, ) { Text( text = formatTime(restTimeInSeconds), style = MaterialTheme.typography.labelLarge, modifier = Modifier.padding( horizontal = 12.dp, vertical = 6.dp, ), ) } } @Composable private fun formatTime(seconds: Long): String = remember(seconds) { val minutes = seconds % 3600 / 60 val secs = seconds % 60 String.format(Locale.getDefault(), "%02d:%02d", minutes, secs) } @Composable private fun StickyHeader( name: String, actions: (@Composable RowScope.() -> Unit)? = null, ) { Surface( color = MaterialTheme.colorScheme.surfaceContainerLow, ) { Row( modifier = Modifier .fillMaxWidth() .heightIn(24.dp) .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( text = name, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.weight(1F)) if (actions != null) { actions() } } } } @Composable private fun SessionError( title: String, message: String, modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( text = title, style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.error, ) Text( text = message, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.outline, ) } } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AddSetSheet( exercise: Exercise, onDismiss: () -> Unit, onAddSet: () -> Unit, ) { val scope = rememberCoroutineScope() val state = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet( sheetState = state, onDismissRequest = onDismiss, containerColor = MaterialTheme.colorScheme.surfaceContainer, ) { AddSet( exercise = exercise, onDone = { onAddSet() scope.launch { state.hide() }.invokeOnCompletion { if (!state.isVisible) onDismiss() } }, ) } } @PreviewLightDark @Composable private fun SessionDetailPreview() { KenkoTheme { val data = remember { SessionDetailState.Success( SessionUiData( date = LocalDate(2024, 4, 15), sets = emptyMap(), isToday = true, ), ) } Surface(modifier = Modifier.fillMaxSize()) { SessionDetail(state = data) } } } @PreviewLightDark @Composable private fun SessionErrorPreview() { KenkoTheme { val data = remember { SessionDetailState.Error.InvalidSession } Surface(modifier = Modifier.fillMaxSize()) { SessionDetail(state = data) } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/sessionDetail/SessionDetailViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.sessionDetail import androidx.annotation.StringRes import androidx.compose.runtime.Stable import androidx.compose.ui.platform.UriHandler import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.looker.kenko.R import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.PlanItem import com.looker.kenko.data.model.Session import com.looker.kenko.data.model.Set import com.looker.kenko.data.model.localDate import com.looker.kenko.data.model.week import com.looker.kenko.data.repository.PlanRepo import com.looker.kenko.data.repository.SessionRepo import com.looker.kenko.data.repository.SettingsRepo import com.looker.kenko.ui.sessionDetail.navigation.SessionDetailRoute import com.looker.kenko.utils.asStateFlow import com.looker.kenko.utils.isToday import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlin.time.Clock import kotlin.time.Instant import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate import kotlinx.datetime.minus @HiltViewModel class SessionDetailViewModel @Inject constructor( private val repo: SessionRepo, private val planRepo: PlanRepo, private val settingsRepo: SettingsRepo, savedStateHandle: SavedStateHandle, private val uriHandler: UriHandler, ) : ViewModel() { private val routeData: SessionDetailRoute = savedStateHandle.toRoute() private val epochDays: Int? = routeData.epochDays.takeIf { it != -1 } private val sessionDate: LocalDate = epochDays?.let { LocalDate.fromEpochDays(it) } ?: localDate val previousSessionDate = sessionDate - week private val previousSessionExists: Flow = repo.streamByDate(previousSessionDate) .map { it != null } private val sessionStream: Flow = repo.streamByDate(sessionDate) @OptIn(ExperimentalCoroutinesApi::class) private val exercisesToday: Flow> = sessionStream.flatMapLatest { session -> if (sessionDate.isToday) { planRepo.activeExercises(sessionDate.dayOfWeek) } else if (session != null) { if (session.planId != null) { planRepo.planItems(session.planId, sessionDate.dayOfWeek) .map { it.map(PlanItem::exercise) } } else { flowOf(session.sets.map { it.exercise }) } } else { flowOf(emptyList()) } } private val lastSetTimeStream = settingsRepo.get { lastSetTime } private val _currentExercise: MutableStateFlow = MutableStateFlow(null) val current: StateFlow = _currentExercise val state: StateFlow = combine( sessionStream, exercisesToday, lastSetTimeStream, previousSessionExists, ) { session, exercises, lastSetTime, previousSession -> if (session == null && epochDays != null) { return@combine SessionDetailState.Error.InvalidSession } if (exercises.isEmpty() && sessionDate.isToday) { return@combine SessionDetailState.Error.EmptyPlan } val currentSession = session ?: Session(-1, emptyList()) val exerciseMap = when { sessionDate.isToday || exercises.isNotEmpty() -> exercises.associateWith { exercise -> currentSession.sets.filter { it.exercise.id == exercise.id } } currentSession.sets.isNotEmpty() -> currentSession.sets.groupBy { it.exercise } else -> emptyMap() } SessionDetailState.Success( SessionUiData( date = currentSession.date, sets = exerciseMap, isToday = currentSession.date.isToday, lastSetTime = lastSetTime, hasPreviousSession = previousSession, ), ) }.onStart { emit(SessionDetailState.Loading) } .asStateFlow(SessionDetailState.Loading) fun startRestTimer() { viewModelScope.launch { settingsRepo.setLastSetTime(Clock.System.now()) } } fun resetRestTimer() { viewModelScope.launch { settingsRepo.setLastSetTime(null) } } fun removeSet(setId: Int?) { if (setId == null) return viewModelScope.launch { repo.removeSet(setId) } } fun showBottomSheet(exercise: Exercise) { viewModelScope.launch { _currentExercise.emit(exercise) } } fun hideSheet() { viewModelScope.launch { _currentExercise.emit(null) } } fun openReference(reference: String) { viewModelScope.launch { try { uriHandler.openUri(reference) } catch (e: IllegalStateException) { e.printStackTrace() } } } } @Stable data class SessionUiData( val date: LocalDate, val sets: Map>, val isToday: Boolean = false, val lastSetTime: Instant? = null, val hasPreviousSession: Boolean = false, ) sealed interface SessionDetailState { data object Loading : SessionDetailState data class Success(val data: SessionUiData) : SessionDetailState sealed class Error( @param:StringRes val title: Int, @param:StringRes val errorMessage: Int, ) : SessionDetailState { data object InvalidSession : Error( title = R.string.label_missed_day, errorMessage = R.string.error_cant_find_session, ) data object EmptyPlan : Error( title = R.string.label_nothing_today, errorMessage = R.string.label_no_exercise_today, ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/sessionDetail/components/SetItem.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.sessionDetail.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp import com.looker.kenko.R import com.looker.kenko.data.local.model.SetType import com.looker.kenko.data.model.Exercise import com.looker.kenko.data.model.ExercisesPreviewParameter import com.looker.kenko.data.model.RepsInReserve import com.looker.kenko.data.model.Set import com.looker.kenko.data.model.repDurationStringRes import com.looker.kenko.ui.theme.KenkoTheme import com.looker.kenko.ui.theme.numbers @Composable fun SetItem( set: Set, modifier: Modifier = Modifier, title: @Composable () -> Unit, ) { Row( modifier = Modifier .heightIn(64.dp) .widthIn(240.dp, 420.dp) .background(MaterialTheme.colorScheme.surface) .then(modifier), verticalAlignment = Alignment.CenterVertically, ) { CompositionLocalProvider( LocalContentColor provides MaterialTheme.colorScheme.outline, LocalTextStyle provides MaterialTheme.typography.displayMedium.numbers(), ) { Box(modifier = Modifier.padding(horizontal = 16.dp)) { title() } } Spacer(modifier = Modifier.width(12.dp)) Row( modifier = Modifier .weight(1F) .clip(MaterialTheme.shapes.large) .background(MaterialTheme.colorScheme.surfaceContainerHigh) .padding(vertical = 16.dp, horizontal = 24.dp), horizontalArrangement = Arrangement.SpaceBetween, ) { PerformedItem( title = stringResource(set.exercise.repDurationStringRes), performance = "${set.repsOrDuration}", ) PerformedItem( title = stringResource(R.string.label_weight), performance = "${set.weight} KG", ) } } } @Composable private fun PerformedItem( title: String, performance: String, modifier: Modifier = Modifier, ) { Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { Text( text = title, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.outline, ) Text( text = performance, style = MaterialTheme.typography.titleMedium, ) } } @PreviewScreenSizes @Composable private fun SetItemPreview( @PreviewParameter(ExercisesPreviewParameter::class, limit = 2) exercises: List, ) { KenkoTheme { SetItem( Set(12, 40F, SetType.Drop, exercises.first(), RepsInReserve(2)), ) { Text(text = "01") } } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/sessionDetail/navigation/SessionDetailNavigation.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.sessionDetail.navigation import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.looker.kenko.ui.sessionDetail.SessionDetails import kotlinx.datetime.LocalDate import kotlinx.serialization.Serializable @Serializable data class SessionDetailRoute( val epochDays: Int, ) fun NavController.navigateToSessionDetail(date: LocalDate?, navOptions: NavOptions? = null) { navigate(SessionDetailRoute(date?.toEpochDays()?.toInt() ?: -1), navOptions) } fun NavGraphBuilder.sessionDetail( onBackPress: () -> Unit, onHistoryClick: (LocalDate) -> Unit, ) { composable { SessionDetails( onBackPress = onBackPress, onHistoryClick = onHistoryClick, viewModel = hiltViewModel(), ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/sessions/Sessions.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.sessions import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FabPosition import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.looker.kenko.R import com.looker.kenko.data.model.Session import com.looker.kenko.ui.components.BackButton import com.looker.kenko.ui.components.EmptyPage import com.looker.kenko.ui.components.TertiaryKenkoButton import com.looker.kenko.ui.extensions.plus import com.looker.kenko.ui.planEdit.components.dayName import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme import com.looker.kenko.utils.DateFormat import com.looker.kenko.utils.formatDate import com.looker.kenko.utils.isToday import kotlinx.datetime.LocalDate @Composable fun Sessions( viewModel: SessionsViewModel, onSessionClick: (LocalDate?) -> Unit, onBackPress: () -> Unit, ) { val state by viewModel.state.collectAsStateWithLifecycle() Sessions( state = state, onSessionClick = onSessionClick, onBackPress = onBackPress, ) } @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun Sessions( state: SessionsUiData, onSessionClick: (LocalDate?) -> Unit, onBackPress: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( modifier = modifier, topBar = { TopAppBar( navigationIcon = { BackButton(onClick = onBackPress) }, title = { Text(text = stringResource(id = R.string.label_sessions_title)) }, ) }, floatingActionButton = { TertiaryKenkoButton( onClick = { onSessionClick(null) }, label = { val isCurrentSessionActive = state.isCurrentSessionActive val text = remember(isCurrentSessionActive) { if (isCurrentSessionActive) { R.string.label_continue_session } else { R.string.label_start_session } } Text(text = stringResource(id = text)) }, icon = { Icon( modifier = Modifier.size(18.dp), painter = KenkoIcons.ArrowOutward, contentDescription = null, ) }, ) }, floatingActionButtonPosition = FabPosition.Center, containerColor = MaterialTheme.colorScheme.surface, ) { padding -> if (state.sessions.isEmpty()) { EmptyPage(stringResource(id = R.string.label_no_sessions)) } else { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = padding + PaddingValues(bottom = 96.dp), verticalArrangement = Arrangement.spacedBy(2.dp), ) { items( items = state.sessions, key = { it.id!! }, ) { session -> SessionCard( modifier = Modifier.padding(horizontal = 14.dp), session = session, onClick = { onSessionClick(session.date) }, ) } } } } } @Composable fun SessionCard( session: Session, modifier: Modifier = Modifier, onClick: () -> Unit = {}, ) { val containerColor = if (session.date.isToday) { MaterialTheme.colorScheme.secondaryContainer } else { MaterialTheme.colorScheme.surfaceContainerLow } val containerShape = if (session.date.isToday) { CircleShape } else { MaterialTheme.shapes.extraLarge } Surface( modifier = modifier, color = containerColor, shape = containerShape, onClick = onClick, ) { Column( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(16.dp), ) { val titleStyle = MaterialTheme.typography.titleLarge val secondaryEmphasis = MaterialTheme.colorScheme.outline val dayName = dayName(session.date.dayOfWeek) val string = remember(session.date, dayName) { buildAnnotatedString { withStyle(titleStyle.toSpanStyle().copy(fontWeight = FontWeight.Bold)) { append(formatDate(session.date, dateTimeFormat = DateFormat.SessionLabel)) } append(" ${Typography.bullet} ") withStyle(titleStyle.toSpanStyle().copy(color = secondaryEmphasis)) { append(dayName) } } } Text(text = string) val exerciseNames = remember(session.performExercises) { session.performExercises.joinToString { it.name } } Text( text = exerciseNames, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline, maxLines = 3, ) } } } @PreviewLightDark @Composable private fun SessionCardPreview() { KenkoTheme { SessionCard( session = Session( planId = 1, date = LocalDate(2024, 4, 15), sets = emptyList(), ), modifier = Modifier.fillMaxWidth(), ) } } @Preview @Composable private fun SessionsPreview() { KenkoTheme { Sessions( state = SessionsUiData(listOf(Session(1, emptyList())), false), onBackPress = {}, onSessionClick = {}, ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/sessions/SessionsViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.sessions import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import com.looker.kenko.data.model.Session import com.looker.kenko.data.model.localDate import com.looker.kenko.data.repository.SessionRepo import com.looker.kenko.utils.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map @HiltViewModel class SessionsViewModel @Inject constructor( repo: SessionRepo, ) : ViewModel() { private val sessionsStream: Flow> = repo.stream private val isCurrentSessionActive: Flow = repo.streamByDate(localDate).map { it != null } val state: StateFlow = combine( sessionsStream, isCurrentSessionActive, ) { sessions, isCurrentSessionActive -> SessionsUiData( sessions = sessions.filter { it.sets.isNotEmpty() }, isCurrentSessionActive = isCurrentSessionActive, ) }.asStateFlow(SessionsUiData(emptyList(), false)) } @Stable data class SessionsUiData( val sessions: List, val isCurrentSessionActive: Boolean, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/sessions/navigation/SessionsPageNavigation.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.sessions.navigation import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.looker.kenko.ui.sessions.Sessions import kotlinx.datetime.LocalDate import kotlinx.serialization.Serializable @Serializable object SessionRoute fun NavController.navigateToSessions(navOptions: NavOptions? = null) { navigate(SessionRoute, navOptions = navOptions) } fun NavGraphBuilder.sessions( onSessionClick: (LocalDate?) -> Unit, onBackPress: () -> Unit, ) { composable { Sessions( onSessionClick = onSessionClick, onBackPress = onBackPress, viewModel = hiltViewModel(), ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/settings/Settings.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.settings import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.Crossfade import androidx.compose.animation.animateColor import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonColors import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRowScope import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.looker.kenko.R import com.looker.kenko.data.model.settings.BackupInterval import com.looker.kenko.data.model.settings.ColorPalettes import com.looker.kenko.data.model.settings.Theme import com.looker.kenko.ui.components.BackButton import com.looker.kenko.ui.components.HealthQuotes import com.looker.kenko.ui.components.KenkoBorderWidth import com.looker.kenko.ui.theme.KenkoIcons import com.looker.kenko.ui.theme.KenkoTheme import com.looker.kenko.ui.theme.colorSchemes.sereneColorSchemes import com.looker.kenko.ui.theme.dynamicColorSchemes import com.looker.kenko.ui.theme.end import com.looker.kenko.ui.theme.start import com.looker.kenko.utils.toFormat import kotlin.time.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime @Composable fun Settings( viewModel: SettingsViewModel, onBackPress: () -> Unit, ) { val state by viewModel.state.collectAsStateWithLifecycle() Settings( state = state, onSelectTheme = viewModel::updateTheme, onSelectColorPalette = viewModel::updateColorPalette, onSelectBackupLocation = viewModel::setBackupLocation, onSelectBackupInterval = viewModel::setBackupInterval, onBackupNow = viewModel::backupNow, onRestore = viewModel::restore, onClearMessage = viewModel::clearBackupMessage, onBackPress = onBackPress, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun Settings( state: SettingsUiData, onSelectTheme: (Theme) -> Unit, onSelectColorPalette: (ColorPalettes) -> Unit, onSelectBackupLocation: (Uri) -> Unit, onSelectBackupInterval: (BackupInterval) -> Unit, onBackupNow: () -> Unit, onRestore: (Uri) -> Unit, onClearMessage: () -> Unit, onBackPress: () -> Unit, modifier: Modifier = Modifier, ) { val snackbarHostState = remember { SnackbarHostState() } val context = LocalContext.current // Handle backup messages LaunchedEffect(state.backupMessage) { state.backupMessage?.let { message -> val text = when (message) { BackupMessage.BackupSuccess -> context.getString(R.string.label_backup_success) BackupMessage.BackupFailed -> context.getString(R.string.error_backup_failed) BackupMessage.RestoreSuccess -> context.getString(R.string.label_restore_success) BackupMessage.RestoreFailed -> context.getString(R.string.error_restore_failed) } snackbarHostState.showSnackbar(text) onClearMessage() } } Scaffold( modifier = modifier, containerColor = Color.Transparent, snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { TopAppBar( colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { BackButton(onClick = onBackPress) }, title = { Text(text = stringResource(R.string.label_settings)) }, ) }, ) { Column( modifier = Modifier .fillMaxSize() .padding(it) .verticalScroll(rememberScrollState()), ) { HorizontalDivider(thickness = KenkoBorderWidth) Spacer(modifier = Modifier.height(16.dp)) CategoryHeader(title = stringResource(R.string.label_theme)) Spacer(modifier = Modifier.height(4.dp)) ThemeButton( modifier = Modifier.align(CenterHorizontally), selectedTheme = state.selectedTheme, onClick = onSelectTheme, ) Spacer(modifier = Modifier.height(16.dp)) CategoryHeader(title = stringResource(R.string.label_color_palettes)) Spacer(modifier = Modifier.height(8.dp)) ColorPaletteSelection( selectedColorPalette = state.selectedColorPalette, selectedTheme = state.selectedTheme, onClickPalette = onSelectColorPalette, ) Spacer(modifier = Modifier.height(24.dp)) CategoryHeader(title = stringResource(R.string.label_backup)) Spacer(modifier = Modifier.height(8.dp)) BackupSection( backupUri = state.backupUri, backupInterval = state.backupInterval, lastBackupTime = state.lastBackupTime, isBackingUp = state.isBackingUp, isRestoring = state.isRestoring, onSelectLocation = onSelectBackupLocation, onSelectInterval = onSelectBackupInterval, onBackupNow = onBackupNow, onRestore = onRestore, ) Spacer(modifier = Modifier.weight(1F)) HealthQuotes(Modifier.align(CenterHorizontally)) } } } @Composable private fun CategoryHeader( title: String, modifier: Modifier = Modifier, ) { Row( modifier = modifier .fillMaxWidth() .padding(start = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { Text(text = title, style = MaterialTheme.typography.titleLarge) Spacer(modifier = Modifier.width(4.dp)) } } @Composable private fun ColorPaletteSelection( selectedColorPalette: ColorPalettes, selectedTheme: Theme, onClickPalette: (ColorPalettes) -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier .fillMaxWidth() .horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Spacer(modifier = Modifier.width(16.dp)) ColorPalettes.entries.forEach { colorPalette -> ColorPaletteItem( isSelected = selectedColorPalette == colorPalette, theme = selectedTheme, colorPalette = colorPalette, modifier = Modifier.clickable { onClickPalette(colorPalette) }, ) } Spacer(modifier = Modifier.width(16.dp)) } } @Composable private fun ColorPaletteItem( isSelected: Boolean, theme: Theme, colorPalette: ColorPalettes, modifier: Modifier = Modifier, ) { val context = LocalContext.current val colorSchemes = remember(colorPalette) { colorPalette.scheme ?: dynamicColorSchemes(context) } if (colorSchemes == null) return val transition = updateTransition(targetState = isSelected, label = null) val corner by transition.animateDp(label = "") { if (it) 32.dp else 16.dp } val background by transition.animateColor(label = "") { if (it) { MaterialTheme.colorScheme.primaryContainer } else { MaterialTheme.colorScheme.surfaceContainerHigh } } val contentColor by transition.animateColor(label = "") { if (it) { MaterialTheme.colorScheme.onPrimaryContainer } else { MaterialTheme.colorScheme.onSurface } } Column( modifier = Modifier .graphicsLayer { clip = true shape = RoundedCornerShape(corner, corner, 16.dp, 16.dp) } .drawBehind { drawRect(background) } .then(modifier), horizontalAlignment = CenterHorizontally, ) { KenkoTheme( theme = theme, colorSchemes = colorSchemes, ) { Box(Modifier.size(80.dp)) { ColorPaletteSample() Crossfade(targetState = isSelected, label = "") { if (it) { Box( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.45f)), contentAlignment = Alignment.Center, ) { Icon( painter = KenkoIcons.Done, contentDescription = null, ) } } } } } Text( modifier = Modifier.padding(vertical = 2.dp), text = stringResource(colorSchemes.nameRes), style = MaterialTheme.typography.labelMedium, color = contentColor, ) } } @Composable private fun ColorPaletteSample( modifier: Modifier = Modifier, ) { Box( modifier = Modifier .size(80.dp) .then(modifier) .padding(8.dp) .clip(CircleShape), ) { Spacer( modifier = Modifier .size(31.dp) .align(Alignment.TopStart) .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(4.dp)), ) Spacer( modifier = Modifier .size(31.dp) .align(Alignment.TopEnd) .background(MaterialTheme.colorScheme.secondary, RoundedCornerShape(4.dp)), ) Spacer( modifier = Modifier .size(64.dp, 31.dp) .align(Alignment.BottomStart) .background(MaterialTheme.colorScheme.tertiary, RoundedCornerShape(4.dp)), ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ThemeButton( selectedTheme: Theme, onClick: (Theme) -> Unit, modifier: Modifier = Modifier, ) { SingleChoiceSegmentedButtonRow(modifier = modifier) { val isSystem = remember(selectedTheme) { selectedTheme == Theme.System } val isDark = remember(selectedTheme) { selectedTheme == Theme.Dark } val isLight = remember(selectedTheme) { selectedTheme == Theme.Light } SystemButton(isSelected = isSystem, onClick = onClick) LightButton(isSelected = isLight, onClick = onClick) DarkButton(isSelected = isDark, onClick = onClick) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SingleChoiceSegmentedButtonRowScope.SystemButton( isSelected: Boolean, onClick: (Theme) -> Unit, ) { val theme = Theme.System SegmentedButton( selected = isSelected, onClick = { onClick(theme) }, shape = CircleShape.end(4.dp), colors = themeButtonColors, modifier = Modifier.padding(2.dp), ) { Text(text = stringResource(theme.nameRes)) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SingleChoiceSegmentedButtonRowScope.LightButton( isSelected: Boolean, onClick: (Theme) -> Unit, ) { val theme = Theme.Light SegmentedButton( selected = isSelected, onClick = { onClick(theme) }, shape = RoundedCornerShape(4.dp), colors = themeButtonColors, modifier = Modifier.padding(2.dp), ) { Text(text = stringResource(theme.nameRes)) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SingleChoiceSegmentedButtonRowScope.DarkButton( isSelected: Boolean, onClick: (Theme) -> Unit, ) { val theme = Theme.Dark SegmentedButton( selected = isSelected, onClick = { onClick(theme) }, shape = CircleShape.start(4.dp), colors = themeButtonColors, modifier = Modifier.padding(2.dp), ) { Text(text = stringResource(theme.nameRes)) } } @OptIn(ExperimentalMaterial3Api::class) private val themeButtonColors: SegmentedButtonColors @Composable get() = SegmentedButtonDefaults.colors( activeBorderColor = Color.Transparent, inactiveBorderColor = Color.Transparent, inactiveContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, ) @Composable private fun BackupSection( backupUri: String?, backupInterval: BackupInterval, lastBackupTime: Instant?, isBackingUp: Boolean, isRestoring: Boolean, onSelectLocation: (Uri) -> Unit, onSelectInterval: (BackupInterval) -> Unit, onBackupNow: () -> Unit, onRestore: (Uri) -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current var showRestoreDialog by remember { mutableStateOf(false) } var pendingRestoreUri by remember { mutableStateOf(null) } val folderPickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.OpenDocumentTree(), ) { uri -> uri?.let { // Take persistable permission context.contentResolver.takePersistableUriPermission( it, android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION or android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION, ) // Store the tree URI - file will be created by BackupManager onSelectLocation(it) } } val filePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.OpenDocument(), ) { uri -> uri?.let { pendingRestoreUri = it showRestoreDialog = true } } if (showRestoreDialog) { RestoreConfirmationDialog( onConfirm = { pendingRestoreUri?.let { onRestore(it) } showRestoreDialog = false pendingRestoreUri = null }, onDismiss = { showRestoreDialog = false pendingRestoreUri = null }, ) } Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(12.dp), ) { BackupSettingRow( title = stringResource(R.string.label_backup_location), value = backupUri?.let { extractFolderName(it) } ?: stringResource(R.string.label_backup_location_not_set), onClick = { folderPickerLauncher.launch(null) }, ) Text( text = stringResource(R.string.label_backup_interval), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 16.dp), ) BackupIntervalSelector( selectedInterval = backupInterval, onSelectInterval = onSelectInterval, enabled = backupUri != null, modifier = Modifier.padding(horizontal = 16.dp), ) if (lastBackupTime != null) { Text( text = stringResource(R.string.label_last_backup, lastBackupTime.toFormat()), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline, modifier = Modifier.padding(horizontal = 16.dp), ) } Row( horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), ) { OutlinedButton( onClick = onBackupNow, enabled = backupUri != null && !isBackingUp && !isRestoring, modifier = Modifier.weight(1f), ) { if (isBackingUp) { CircularProgressIndicator( modifier = Modifier.size(16.dp), strokeWidth = 2.dp, ) Spacer(modifier = Modifier.width(8.dp)) } Text( text = if (isBackingUp) { stringResource(R.string.label_backup_in_progress) } else { stringResource(R.string.label_backup_now) }, ) } OutlinedButton( onClick = { filePickerLauncher.launch(arrayOf("application/zip")) }, enabled = !isBackingUp && !isRestoring, modifier = Modifier.weight(1f), ) { if (isRestoring) { CircularProgressIndicator( modifier = Modifier.size(16.dp), strokeWidth = 2.dp, ) Spacer(modifier = Modifier.width(8.dp)) } Text( text = if (isRestoring) { stringResource(R.string.label_restore_in_progress) } else { stringResource(R.string.label_restore) }, ) } } } } @Composable private fun BackupSettingRow( title: String, value: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier .fillMaxWidth() .clickable(onClick = onClick) .padding(vertical = 8.dp, horizontal = 16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1f)) { Text( text = title, style = MaterialTheme.typography.bodyMedium, ) Text( text = value, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline, ) } Icon( painter = KenkoIcons.ArrowForward, contentDescription = null, tint = MaterialTheme.colorScheme.outline, ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun BackupIntervalSelector( selectedInterval: BackupInterval, onSelectInterval: (BackupInterval) -> Unit, enabled: Boolean, modifier: Modifier = Modifier, ) { SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) { BackupInterval.entries.forEachIndexed { index, interval -> SegmentedButton( selected = selectedInterval == interval, onClick = { onSelectInterval(interval) }, enabled = enabled, shape = when (index) { 0 -> CircleShape.end(4.dp) BackupInterval.entries.lastIndex -> CircleShape.start(4.dp) else -> RoundedCornerShape(4.dp) }, colors = themeButtonColors, modifier = Modifier.padding(2.dp), ) { Text( text = stringResource(interval.nameRes), style = MaterialTheme.typography.labelSmall, ) } } } } @Composable private fun RestoreConfirmationDialog( onConfirm: () -> Unit, onDismiss: () -> Unit, ) { AlertDialog( onDismissRequest = onDismiss, title = { Text(stringResource(R.string.label_restore)) }, text = { Text(stringResource(R.string.label_restore_warning)) }, confirmButton = { Button(onClick = onConfirm) { Text(stringResource(R.string.label_yes)) } }, dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.label_no)) } }, ) } private fun extractFolderName(uri: String): String { return try { uri.toUri().lastPathSegment?.substringAfterLast('/') ?: uri } catch (_: Exception) { uri } } private fun formatBackupTime(instant: Instant): String { val localDateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault()) return "${localDateTime.date} ${localDateTime.hour}:${ localDateTime.minute.toString().padStart(2, '0') }" } @Preview @Composable private fun ColorSelectionPreview() { KenkoTheme(colorSchemes = sereneColorSchemes) { var isSelected by remember { mutableStateOf(false) } ColorPaletteItem( modifier = Modifier.clickable { isSelected = !isSelected }, isSelected = isSelected, theme = Theme.System, colorPalette = ColorPalettes.Serene, ) } } @Preview @Composable private fun ThemePreview() { KenkoTheme { ThemeButton(selectedTheme = Theme.System, onClick = {}) } } @Preview @Composable private fun SettingsPreview() { KenkoTheme { Settings( state = SettingsUiData( selectedTheme = Theme.System, selectedColorPalette = ColorPalettes.Default, backupUri = null, backupInterval = BackupInterval.Off, lastBackupTime = null, isBackingUp = false, isRestoring = false, backupMessage = null, ), onSelectTheme = {}, onSelectColorPalette = {}, onSelectBackupLocation = {}, onSelectBackupInterval = {}, onBackupNow = {}, onRestore = {}, onClearMessage = {}, onBackPress = {}, ) } } @Preview @Composable private fun BackupSectionPreview() { KenkoTheme { BackupSection( backupUri = "content://com.android.providers.downloads/tree/downloads", backupInterval = BackupInterval.Daily, lastBackupTime = null, isBackingUp = false, isRestoring = false, onSelectLocation = {}, onSelectInterval = {}, onBackupNow = {}, onRestore = {}, ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/settings/SettingsViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.settings import android.net.Uri import androidx.compose.runtime.Stable import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.looker.kenko.data.backup.BackupManager import com.looker.kenko.data.backup.BackupResult import com.looker.kenko.data.model.settings.BackupInterval import com.looker.kenko.data.model.settings.ColorPalettes import com.looker.kenko.data.model.settings.Theme import com.looker.kenko.data.repository.SettingsRepo import com.looker.kenko.utils.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlin.time.Clock import kotlin.time.Instant import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @HiltViewModel class SettingsViewModel @Inject constructor( private val repo: SettingsRepo, private val backupManager: BackupManager, ) : ViewModel() { private val _backupState = MutableStateFlow(BackupState()) val state: StateFlow = combine( repo.stream, _backupState, ) { settings, backupState -> SettingsUiData( selectedTheme = settings.theme, selectedColorPalette = settings.colorPalette, backupUri = settings.backupUri, backupInterval = settings.backupInterval, lastBackupTime = settings.lastBackupTime, isBackingUp = backupState.isBackingUp, isRestoring = backupState.isRestoring, backupMessage = backupState.message, ) }.asStateFlow( SettingsUiData( selectedTheme = Theme.System, selectedColorPalette = ColorPalettes.Default, backupUri = null, backupInterval = BackupInterval.Off, lastBackupTime = null, isBackingUp = false, isRestoring = false, backupMessage = null, ), ) fun updateTheme(theme: Theme) { viewModelScope.launch { repo.setTheme(theme) } } fun updateColorPalette(colorPalette: ColorPalettes) { viewModelScope.launch { repo.setColorPalette(colorPalette) } } fun setBackupLocation(uri: Uri) { viewModelScope.launch { repo.setBackupUri(uri.toString()) backupManager.schedulePeriodicBackup(state.value.backupInterval, uri) } } fun setBackupInterval(interval: BackupInterval) { viewModelScope.launch { repo.setBackupInterval(interval) val backupUri = state.value.backupUri?.toUri() if (backupUri != null) { backupManager.schedulePeriodicBackup(interval, backupUri) } } } fun backupNow() { val backupUri = state.value.backupUri?.toUri() ?: return viewModelScope.launch { _backupState.update { it.copy(isBackingUp = true, message = null) } when (backupManager.createBackup(backupUri)) { is BackupResult.Success -> { repo.setLastBackupTime(Clock.System.now()) _backupState.update { it.copy(isBackingUp = false, message = BackupMessage.BackupSuccess) } } is BackupResult.Error -> { _backupState.update { it.copy(isBackingUp = false, message = BackupMessage.BackupFailed) } } } } } fun restore(uri: Uri) { viewModelScope.launch { _backupState.update { it.copy(isRestoring = true, message = null) } when (backupManager.restoreBackup(uri)) { is BackupResult.Success -> { repo.setBackupUri(null) repo.setLastBackupTime(null) _backupState.update { it.copy(isRestoring = false, message = BackupMessage.RestoreSuccess) } } is BackupResult.Error -> { _backupState.update { it.copy(isRestoring = false, message = BackupMessage.RestoreFailed) } } } } } fun clearBackupMessage() { _backupState.update { it.copy(message = null) } } } private data class BackupState( val isBackingUp: Boolean = false, val isRestoring: Boolean = false, val message: BackupMessage? = null, ) enum class BackupMessage { BackupSuccess, BackupFailed, RestoreSuccess, RestoreFailed, } @Stable data class SettingsUiData( val selectedTheme: Theme, val selectedColorPalette: ColorPalettes, val backupUri: String?, val backupInterval: BackupInterval, val lastBackupTime: Instant?, val isBackingUp: Boolean, val isRestoring: Boolean, val backupMessage: BackupMessage?, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/settings/navigation/SettingsNavigation.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.settings.navigation import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.looker.kenko.ui.settings.Settings import kotlinx.serialization.Serializable @Serializable object SettingsRoute fun NavController.navigateToSettings(navOptions: NavOptions? = null) { navigate(SettingsRoute, navOptions) } fun NavGraphBuilder.settings( onBackPress: () -> Unit, ) { composable { Settings( onBackPress = onBackPress, viewModel = hiltViewModel(), ) } } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/theme/KenkoIcons.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.theme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import com.looker.kenko.R /** * Material Symbols Settings * * Weight: 300 * Grade: 0 * Optical Size: 24px * Style: Rounded * Fill: False */ object KenkoIcons { val ArrowBack: Painter @Composable get() = painterResource(R.drawable.ic_arrow_back) val ArrowForward: Painter @Composable get() = painterResource(R.drawable.ic_arrow_forward) val ArrowOutward: Painter @Composable get() = painterResource(R.drawable.ic_arrow_outward) val Circle: Painter @Composable get() = painterResource(R.drawable.ic_radio_button_unchecked) val Lightbulb: Painter @Composable get() = painterResource(R.drawable.ic_lightbulb) val Add: Painter @Composable get() = painterResource(R.drawable.ic_add) val Info: Painter @Composable get() = painterResource(R.drawable.ic_info) val Done: Painter @Composable get() = painterResource(R.drawable.ic_check) val History: Painter @Composable get() = painterResource(R.drawable.ic_history) val Delete: Painter @Composable get() = painterResource(R.drawable.ic_delete) val Remove: Painter @Composable get() = painterResource(R.drawable.ic_remove) val Save: Painter @Composable get() = painterResource(R.drawable.ic_save) val Rename: Painter @Composable get() = painterResource(R.drawable.ic_edit) val Plan: Painter @Composable get() = painterResource(R.drawable.ic_tactic) val Home: Painter @Composable get() = painterResource(R.drawable.ic_home) val Person: Painter @Composable get() = painterResource(R.drawable.ic_person) val Performance: Painter @Composable get() = painterResource(R.drawable.ic_show_chart) val Settings: Painter @Composable get() = painterResource(R.drawable.ic_settings) val KeyboardArrowRight: Painter @Composable get() = painterResource(R.drawable.ic_keyboard_arrow_right) val KeyboardArrowLeft: Painter @Composable get() = painterResource(R.drawable.ic_keyboard_arrow_left) // Brutalist Icons val AddLarge: ImageVector = com.looker.kenko.ui.components.icons.AddLarge val ArrowOutwardLarge: ImageVector = com.looker.kenko.ui.components.icons.ArrowOutwardLarge val Cloud: ImageVector = com.looker.kenko.ui.components.icons.Cloud val Colony: ImageVector = com.looker.kenko.ui.components.icons.Colony val Arrow1: ImageVector = com.looker.kenko.ui.components.icons.Arrow1 val Arrow2: ImageVector = com.looker.kenko.ui.components.icons.Arrow2 val Arrow3: ImageVector = com.looker.kenko.ui.components.icons.Arrow3 val Arrow4: ImageVector = com.looker.kenko.ui.components.icons.Arrow4 val Dawn: ImageVector = com.looker.kenko.ui.components.icons.Dawn val ConcentricTriangles: ImageVector = com.looker.kenko.ui.components.icons.ConcentricTriangles val Stack: ImageVector = com.looker.kenko.ui.components.icons.Stack val Reveal: ImageVector = com.looker.kenko.ui.components.icons.Reveal val QuarterCircles: ImageVector = com.looker.kenko.ui.components.icons.QuarterCircles val Wireframe: ImageVector = com.looker.kenko.ui.components.icons.Wireframe } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/theme/Shapes.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.theme import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp val Shapes = Shapes( extraSmall = RoundedCornerShape(4.dp), small = RoundedCornerShape(8.dp), medium = RoundedCornerShape(14.dp), large = RoundedCornerShape(20.dp), extraLarge = RoundedCornerShape(28.dp), ) fun CornerBasedShape.end( bottomEnd: Dp = 0.dp, topEnd: Dp = bottomEnd, ): CornerBasedShape = copy(bottomEnd = CornerSize(bottomEnd), topEnd = CornerSize(topEnd)) fun CornerBasedShape.start( bottomStart: Dp = 0.dp, topStart: Dp = bottomStart, ): CornerBasedShape = copy(bottomStart = CornerSize(bottomStart), topStart = CornerSize(topStart)) fun CornerBasedShape.end( end: CornerBasedShape, ): CornerBasedShape = copy(bottomEnd = end.bottomEnd, topEnd = end.topEnd) fun CornerBasedShape.start( start: CornerBasedShape, ): CornerBasedShape = copy(bottomStart = start.bottomStart, topStart = start.topStart) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/theme/Theme.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.theme import android.app.Activity import android.content.Context import android.os.Build import android.view.View import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat import com.looker.kenko.R import com.looker.kenko.data.model.settings.Theme import com.looker.kenko.ui.theme.colorSchemes.ColorSchemes import com.looker.kenko.ui.theme.colorSchemes.zestfulColorSchemes @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun KenkoTheme( theme: Theme = Theme.System, colorSchemes: ColorSchemes = zestfulColorSchemes, content: @Composable () -> Unit, ) { val systemTheme = isSystemInDarkTheme() val isDarkTheme = remember(theme) { when (theme) { Theme.System -> systemTheme Theme.Light -> false Theme.Dark -> true } } val colorScheme = if (isDarkTheme) { colorSchemes.dark } else { colorSchemes.light } val localView = LocalView.current SideEffect { setupSystemBar(localView, isDarkTheme) } MaterialExpressiveTheme( colorScheme = colorScheme, typography = Typography, shapes = Shapes, content = content, ) } fun setupSystemBar(view: View, isDarkTheme: Boolean) { if (view.isInEditMode) return val window = (view.context as Activity).window with(WindowCompat.getInsetsController(window, view)) { isAppearanceLightStatusBars = !isDarkTheme isAppearanceLightNavigationBars = !isDarkTheme } } fun dynamicColorSchemes(context: Context): ColorSchemes? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { ColorSchemes( light = dynamicLightColorScheme(context), dark = dynamicDarkColorScheme(context), nameRes = R.string.label_color_scheme_dynamic, ) } else { null } ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/theme/Type.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import com.looker.kenko.R val FontFamily.Companion.Numbers get() = FontFamily(Font(R.font.spacemono_bold)) val displayFont = FontFamily( Font(R.font.darkergrotesque_bold, weight = FontWeight.Bold), Font(R.font.darkergrotesque_semibold, weight = FontWeight.SemiBold), ) val bodyFont = FontFamily( Font(R.font.spacemono_bold, weight = FontWeight.Bold), Font(R.font.spacemono_normal, weight = FontWeight.Normal), ) fun Typography.header() = displayLarge.copy( fontSize = 78.sp, lineHeight = 70.sp, ) fun TextStyle.numbers() = copy(fontFamily = FontFamily.Numbers) val baseline = Typography() val Typography = Typography().copy( displayLarge = baseline.displayLarge.copy( fontFamily = displayFont, fontWeight = FontWeight.Bold, ), displayMedium = baseline.displayMedium.copy( fontFamily = displayFont, fontWeight = FontWeight.Bold, lineHeight = 45.sp, ), displaySmall = baseline.displaySmall.copy( fontFamily = displayFont, fontWeight = FontWeight.SemiBold, ), headlineLarge = baseline.headlineLarge.copy( fontFamily = displayFont, fontWeight = FontWeight.Bold, ), headlineMedium = baseline.headlineMedium.copy( fontFamily = displayFont, fontWeight = FontWeight.Bold, ), headlineSmall = baseline.headlineSmall.copy( fontFamily = displayFont, fontWeight = FontWeight.SemiBold, ), titleLarge = baseline.titleLarge.copy( fontFamily = displayFont, fontWeight = FontWeight.SemiBold, ), titleMedium = baseline.titleMedium.copy( fontFamily = displayFont, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, ), titleSmall = baseline.titleSmall.copy( fontFamily = displayFont, fontWeight = FontWeight.SemiBold, ), bodyLarge = baseline.bodyLarge.copy( fontFamily = bodyFont, fontWeight = FontWeight.Normal, ), bodyMedium = baseline.bodyMedium.copy( fontFamily = bodyFont, fontWeight = FontWeight.Normal, ), bodySmall = baseline.bodySmall.copy( fontFamily = bodyFont, fontWeight = FontWeight.Normal, ), labelLarge = baseline.labelLarge.copy( fontFamily = bodyFont, fontWeight = FontWeight.Normal, ), labelMedium = baseline.labelMedium.copy( fontFamily = bodyFont, fontWeight = FontWeight.Normal, ), labelSmall = baseline.labelSmall.copy( fontFamily = bodyFont, fontWeight = FontWeight.Normal, ), ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/theme/colorSchemes/ColorSchemes.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.theme.colorSchemes import androidx.annotation.StringRes import androidx.compose.material3.ColorScheme import androidx.compose.runtime.Immutable @Immutable data class ColorSchemes( val light: ColorScheme, val dark: ColorScheme, val mediumContrastLight: ColorScheme? = null, val mediumContrastDark: ColorScheme? = null, val highContrastLight: ColorScheme? = null, val highContrastDark: ColorScheme? = null, @StringRes val nameRes: Int, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/theme/colorSchemes/Default.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.theme.colorSchemes import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.ui.graphics.Color import com.looker.kenko.R // Green & Brown private val primaryLight = Color(0xFF5D6146) private val onPrimaryLight = Color(0xFFFDFFDC) private val primaryContainerLight = Color(0xFFE2E5C3) private val onPrimaryContainerLight = Color(0xFF2F321B) private val secondaryLight = Color(0xFF5F5F53) private val onSecondaryLight = Color(0xFFFDFFDC) private val secondaryContainerLight = Color(0xFFE5E4D4) private val onSecondaryContainerLight = Color(0xFF48493D) private val tertiaryLight = Color(0xFF6E3D00) private val onTertiaryLight = Color(0xFFFFDCBF) private val tertiaryContainerLight = Color(0xFF9E5E12) private val onTertiaryContainerLight = Color(0xFFFFEEE1) private val errorLight = Color(0xFFBA1A1A) private val onErrorLight = Color(0xFFFFEDEA) private val errorContainerLight = Color(0xFFFFDAD6) private val onErrorContainerLight = Color(0xFF410002) private val backgroundLight = Color(0xFFFCF9F5) private val onBackgroundLight = Color(0xFF1C1C19) private val surfaceLight = Color(0xFFFCF9F5) private val onSurfaceLight = Color(0xFF1C1C19) private val surfaceVariantLight = Color(0xFFE4E3D6) private val onSurfaceVariantLight = Color(0xFF47473E) private val outlineLight = Color(0xFF78786D) private val outlineVariantLight = Color(0xFFC8C7BB) private val scrimLight = Color(0xFF000000) private val inverseSurfaceLight = Color(0xFF31302E) private val inverseOnSurfaceLight = Color(0xFFF4F0EC) private val inversePrimaryLight = Color(0xFFC6C9A9) private val surfaceDimLight = Color(0xFFDDD9D5) private val surfaceBrightLight = Color(0xFFFCF9F5) private val surfaceContainerLowestLight = Color(0xFFFFFFFF) private val surfaceContainerLowLight = Color(0xFFF7F3EF) private val surfaceContainerLight = Color(0xFFF1EDE9) private val surfaceContainerHighLight = Color(0xFFEBE8E3) private val surfaceContainerHighestLight = Color(0xFFE5E2DE) private val primaryDark = Color(0xFFC6C9A9) private val onPrimaryDark = Color(0xFF2F321B) private val primaryContainerDark = Color(0xFF464930) private val onPrimaryContainerDark = Color(0xFFBFC2AE) private val secondaryDark = Color(0xFFC8C7B8) private val onSecondaryDark = Color(0xFF303126) private val secondaryContainerDark = Color(0xFF3D3E33) private val onSecondaryContainerDark = Color(0xFFD3D2C2) private val tertiaryDark = Color(0xFFFFB873) private val onTertiaryDark = Color(0xFF4A2800) private val tertiaryContainerDark = Color(0xFF7F4800) private val onTertiaryContainerDark = Color(0xFFFFF8F5) private val errorDark = Color(0xFFFFB4AB) private val onErrorDark = Color(0xFF690005) private val errorContainerDark = Color(0xFF93000A) private val onErrorContainerDark = Color(0xFFFFDAD6) private val backgroundDark = Color(0xFF141311) private val onBackgroundDark = Color(0xFFE5E2DE) private val surfaceDark = Color(0xFF141311) private val onSurfaceDark = Color(0xFFE5E2DE) private val surfaceVariantDark = Color(0xFF47473E) private val onSurfaceVariantDark = Color(0xFFC8C7BB) private val outlineDark = Color(0xFF929186) private val outlineVariantDark = Color(0xFF47473E) private val scrimDark = Color(0xFF000000) private val inverseSurfaceDark = Color(0xFFE0DCD5) private val inverseOnSurfaceDark = Color(0xFF31302E) private val inversePrimaryDark = Color(0xFF5D6146) private val surfaceDimDark = Color(0xFF141311) private val surfaceBrightDark = Color(0xFF3A3936) private val surfaceContainerLowestDark = Color(0xFF0E0E0C) private val surfaceContainerLowDark = Color(0xFF1C1C19) private val surfaceContainerDark = Color(0xFF20201D) private val surfaceContainerHighDark = Color(0xFF2A2A27) private val surfaceContainerHighestDark = Color(0xFF353532) private val JapanRed = Color(0xFFE03523) private val defaultLightScheme = lightColorScheme( primary = primaryLight, onPrimary = onPrimaryLight, primaryContainer = primaryContainerLight, onPrimaryContainer = onPrimaryContainerLight, secondary = secondaryLight, onSecondary = onSecondaryLight, secondaryContainer = secondaryContainerLight, onSecondaryContainer = onSecondaryContainerLight, tertiary = tertiaryLight, onTertiary = onTertiaryLight, tertiaryContainer = tertiaryContainerLight, onTertiaryContainer = onTertiaryContainerLight, error = errorLight, onError = onErrorLight, errorContainer = errorContainerLight, onErrorContainer = onErrorContainerLight, background = backgroundLight, onBackground = onBackgroundLight, surface = surfaceLight, onSurface = onSurfaceLight, surfaceVariant = surfaceVariantLight, onSurfaceVariant = onSurfaceVariantLight, outline = outlineLight, outlineVariant = outlineVariantLight, scrim = scrimLight, inverseSurface = inverseSurfaceLight, inverseOnSurface = inverseOnSurfaceLight, inversePrimary = inversePrimaryLight, surfaceDim = surfaceDimLight, surfaceBright = surfaceBrightLight, surfaceContainerLowest = surfaceContainerLowestLight, surfaceContainerLow = surfaceContainerLowLight, surfaceContainer = surfaceContainerLight, surfaceContainerHigh = surfaceContainerHighLight, surfaceContainerHighest = surfaceContainerHighestLight, ) private val defaultDarkScheme = darkColorScheme( primary = primaryDark, onPrimary = onPrimaryDark, primaryContainer = primaryContainerDark, onPrimaryContainer = onPrimaryContainerDark, secondary = secondaryDark, onSecondary = onSecondaryDark, secondaryContainer = secondaryContainerDark, onSecondaryContainer = onSecondaryContainerDark, tertiary = tertiaryDark, onTertiary = onTertiaryDark, tertiaryContainer = tertiaryContainerDark, onTertiaryContainer = onTertiaryContainerDark, error = errorDark, onError = onErrorDark, errorContainer = errorContainerDark, onErrorContainer = onErrorContainerDark, background = backgroundDark, onBackground = onBackgroundDark, surface = surfaceDark, onSurface = onSurfaceDark, surfaceVariant = surfaceVariantDark, onSurfaceVariant = onSurfaceVariantDark, outline = outlineDark, outlineVariant = outlineVariantDark, scrim = scrimDark, inverseSurface = inverseSurfaceDark, inverseOnSurface = inverseOnSurfaceDark, inversePrimary = inversePrimaryDark, surfaceDim = surfaceDimDark, surfaceBright = surfaceBrightDark, surfaceContainerLowest = surfaceContainerLowestDark, surfaceContainerLow = surfaceContainerLowDark, surfaceContainer = surfaceContainerDark, surfaceContainerHigh = surfaceContainerHighDark, surfaceContainerHighest = surfaceContainerHighestDark, ) val defaultColorSchemes = ColorSchemes( light = defaultLightScheme, dark = defaultDarkScheme, nameRes = R.string.label_color_scheme_default, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/theme/colorSchemes/Serene.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.theme.colorSchemes import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.ui.graphics.Color import com.looker.kenko.R // Blue & Pink private val primaryLight = Color(0xFF0005B8) private val onPrimaryLight = Color(0xFFFFFFFF) private val primaryContainerLight = Color(0xFF2631FC) private val onPrimaryContainerLight = Color(0xFFFFFDFF) private val secondaryLight = Color(0xFF4E55B0) private val onSecondaryLight = Color(0xFFFFFFFF) private val secondaryContainerLight = Color(0xFFA4AAFF) private val onSecondaryContainerLight = Color(0xFF0F1475) private val tertiaryLight = Color(0xFF5D0071) private val onTertiaryLight = Color(0xFFFFFFFF) private val tertiaryContainerLight = Color(0xFF8E20A7) private val onTertiaryContainerLight = Color(0xFFFFFCFF) private val errorLight = Color(0xFFBA1A1A) private val onErrorLight = Color(0xFFFFFFFF) private val errorContainerLight = Color(0xFFFFDAD6) private val onErrorContainerLight = Color(0xFF410002) private val backgroundLight = Color(0xFFFBF8FF) private val onBackgroundLight = Color(0xFF1A1B25) private val surfaceLight = Color(0xFFFBF8FF) private val onSurfaceLight = Color(0xFF1A1B25) private val surfaceVariantLight = Color(0xFFE2E0F7) private val onSurfaceVariantLight = Color(0xFF454557) private val outlineLight = Color(0xFF757589) private val outlineVariantLight = Color(0xFFC5C4DA) private val scrimLight = Color(0xFF000000) private val inverseSurfaceLight = Color(0xFF2F2F3B) private val inverseOnSurfaceLight = Color(0xFFF1EFFE) private val inversePrimaryLight = Color(0xFFBEC2FF) private val surfaceDimLight = Color(0xFFDAD8E7) private val surfaceBrightLight = Color(0xFFFBF8FF) private val surfaceContainerLowestLight = Color(0xFFFFFFFF) private val surfaceContainerLowLight = Color(0xFFF4F2FF) private val surfaceContainerLight = Color(0xFFEEECFB) private val surfaceContainerHighLight = Color(0xFFE9E6F6) private val surfaceContainerHighestLight = Color(0xFFE3E1F0) private val primaryLightMediumContrast = Color(0xFF0005B8) private val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) private val primaryContainerLightMediumContrast = Color(0xFF2631FC) private val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) private val secondaryLightMediumContrast = Color(0xFF313892) private val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) private val secondaryContainerLightMediumContrast = Color(0xFF656CC8) private val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) private val tertiaryLightMediumContrast = Color(0xFF5D0071) private val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) private val tertiaryContainerLightMediumContrast = Color(0xFF8E20A7) private val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) private val errorLightMediumContrast = Color(0xFF8C0009) private val onErrorLightMediumContrast = Color(0xFFFFFFFF) private val errorContainerLightMediumContrast = Color(0xFFDA342E) private val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) private val backgroundLightMediumContrast = Color(0xFFFBF8FF) private val onBackgroundLightMediumContrast = Color(0xFF1A1B25) private val surfaceLightMediumContrast = Color(0xFFFBF8FF) private val onSurfaceLightMediumContrast = Color(0xFF1A1B25) private val surfaceVariantLightMediumContrast = Color(0xFFE2E0F7) private val onSurfaceVariantLightMediumContrast = Color(0xFF414153) private val outlineLightMediumContrast = Color(0xFF5D5E70) private val outlineVariantLightMediumContrast = Color(0xFF79798C) private val scrimLightMediumContrast = Color(0xFF000000) private val inverseSurfaceLightMediumContrast = Color(0xFF2F2F3B) private val inverseOnSurfaceLightMediumContrast = Color(0xFFF1EFFE) private val inversePrimaryLightMediumContrast = Color(0xFFBEC2FF) private val surfaceDimLightMediumContrast = Color(0xFFDAD8E7) private val surfaceBrightLightMediumContrast = Color(0xFFFBF8FF) private val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) private val surfaceContainerLowLightMediumContrast = Color(0xFFF4F2FF) private val surfaceContainerLightMediumContrast = Color(0xFFEEECFB) private val surfaceContainerHighLightMediumContrast = Color(0xFFE9E6F6) private val surfaceContainerHighestLightMediumContrast = Color(0xFFE3E1F0) private val primaryLightHighContrast = Color(0xFF000380) private val onPrimaryLightHighContrast = Color(0xFFFFFFFF) private val primaryContainerLightHighContrast = Color(0xFF0008E0) private val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) private val secondaryLightHighContrast = Color(0xFF090F72) private val onSecondaryLightHighContrast = Color(0xFFFFFFFF) private val secondaryContainerLightHighContrast = Color(0xFF313892) private val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) private val tertiaryLightHighContrast = Color(0xFF3F004E) private val onTertiaryLightHighContrast = Color(0xFFFFFFFF) private val tertiaryContainerLightHighContrast = Color(0xFF73008C) private val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) private val errorLightHighContrast = Color(0xFF4E0002) private val onErrorLightHighContrast = Color(0xFFFFFFFF) private val errorContainerLightHighContrast = Color(0xFF8C0009) private val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) private val backgroundLightHighContrast = Color(0xFFFBF8FF) private val onBackgroundLightHighContrast = Color(0xFF1A1B25) private val surfaceLightHighContrast = Color(0xFFFBF8FF) private val onSurfaceLightHighContrast = Color(0xFF000000) private val surfaceVariantLightHighContrast = Color(0xFFE2E0F7) private val onSurfaceVariantLightHighContrast = Color(0xFF222333) private val outlineLightHighContrast = Color(0xFF414153) private val outlineVariantLightHighContrast = Color(0xFF414153) private val scrimLightHighContrast = Color(0xFF000000) private val inverseSurfaceLightHighContrast = Color(0xFF2F2F3B) private val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) private val inversePrimaryLightHighContrast = Color(0xFFEBEAFF) private val surfaceDimLightHighContrast = Color(0xFFDAD8E7) private val surfaceBrightLightHighContrast = Color(0xFFFBF8FF) private val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) private val surfaceContainerLowLightHighContrast = Color(0xFFF4F2FF) private val surfaceContainerLightHighContrast = Color(0xFFEEECFB) private val surfaceContainerHighLightHighContrast = Color(0xFFE9E6F6) private val surfaceContainerHighestLightHighContrast = Color(0xFFE3E1F0) private val primaryDark = Color(0xFFBEC2FF) private val onPrimaryDark = Color(0xFF0004AA) private val primaryContainerDark = Color(0xFF0007D8) private val onPrimaryContainerDark = Color(0xFFD0D2FF) private val secondaryDark = Color(0xFFBEC2FF) private val onSecondaryDark = Color(0xFF1D237F) private val secondaryContainerDark = Color(0xFF2E358F) private val onSecondaryContainerDark = Color(0xFFCFD1FF) private val tertiaryDark = Color(0xFFF6ADFF) private val onTertiaryDark = Color(0xFF560069) private val tertiaryContainerDark = Color(0xFF6F0087) private val onTertiaryContainerDark = Color(0xFFFAC3FF) private val errorDark = Color(0xFFFFB4AB) private val onErrorDark = Color(0xFF690005) private val errorContainerDark = Color(0xFF93000A) private val onErrorContainerDark = Color(0xFFFFDAD6) private val backgroundDark = Color(0xFF12131D) private val onBackgroundDark = Color(0xFFE3E1F0) private val surfaceDark = Color(0xFF12131D) private val onSurfaceDark = Color(0xFFE3E1F0) private val surfaceVariantDark = Color(0xFF454557) private val onSurfaceVariantDark = Color(0xFFC5C4DA) private val outlineDark = Color(0xFF8F8FA3) private val outlineVariantDark = Color(0xFF454557) private val scrimDark = Color(0xFF000000) private val inverseSurfaceDark = Color(0xFFE3E1F0) private val inverseOnSurfaceDark = Color(0xFF2F2F3B) private val inversePrimaryDark = Color(0xFF323DFF) private val surfaceDimDark = Color(0xFF12131D) private val surfaceBrightDark = Color(0xFF383844) private val surfaceContainerLowestDark = Color(0xFF0D0D17) private val surfaceContainerLowDark = Color(0xFF1A1B25) private val surfaceContainerDark = Color(0xFF1E1F29) private val surfaceContainerHighDark = Color(0xFF292934) private val surfaceContainerHighestDark = Color(0xFF34343F) private val primaryDarkMediumContrast = Color(0xFFC3C6FF) private val onPrimaryDarkMediumContrast = Color(0xFF00015D) private val primaryContainerDarkMediumContrast = Color(0xFF7B85FF) private val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) private val secondaryDarkMediumContrast = Color(0xFFC3C6FF) private val onSecondaryDarkMediumContrast = Color(0xFF00015D) private val secondaryContainerDarkMediumContrast = Color(0xFF8188E7) private val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) private val tertiaryDarkMediumContrast = Color(0xFFF7B3FF) private val onTertiaryDarkMediumContrast = Color(0xFF2C0037) private val tertiaryContainerDarkMediumContrast = Color(0xFFD063E7) private val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) private val errorDarkMediumContrast = Color(0xFFFFBAB1) private val onErrorDarkMediumContrast = Color(0xFF370001) private val errorContainerDarkMediumContrast = Color(0xFFFF5449) private val onErrorContainerDarkMediumContrast = Color(0xFF000000) private val backgroundDarkMediumContrast = Color(0xFF12131D) private val onBackgroundDarkMediumContrast = Color(0xFFE3E1F0) private val surfaceDarkMediumContrast = Color(0xFF12131D) private val onSurfaceDarkMediumContrast = Color(0xFFFDF9FF) private val surfaceVariantDarkMediumContrast = Color(0xFF454557) private val onSurfaceVariantDarkMediumContrast = Color(0xFFCAC9DE) private val outlineDarkMediumContrast = Color(0xFFA1A1B6) private val outlineVariantDarkMediumContrast = Color(0xFF818195) private val scrimDarkMediumContrast = Color(0xFF000000) private val inverseSurfaceDarkMediumContrast = Color(0xFFE3E1F0) private val inverseOnSurfaceDarkMediumContrast = Color(0xFF292934) private val inversePrimaryDarkMediumContrast = Color(0xFF020BEE) private val surfaceDimDarkMediumContrast = Color(0xFF12131D) private val surfaceBrightDarkMediumContrast = Color(0xFF383844) private val surfaceContainerLowestDarkMediumContrast = Color(0xFF0D0D17) private val surfaceContainerLowDarkMediumContrast = Color(0xFF1A1B25) private val surfaceContainerDarkMediumContrast = Color(0xFF1E1F29) private val surfaceContainerHighDarkMediumContrast = Color(0xFF292934) private val surfaceContainerHighestDarkMediumContrast = Color(0xFF34343F) private val primaryDarkHighContrast = Color(0xFFFDF9FF) private val onPrimaryDarkHighContrast = Color(0xFF000000) private val primaryContainerDarkHighContrast = Color(0xFFC3C6FF) private val onPrimaryContainerDarkHighContrast = Color(0xFF000000) private val secondaryDarkHighContrast = Color(0xFFFDF9FF) private val onSecondaryDarkHighContrast = Color(0xFF000000) private val secondaryContainerDarkHighContrast = Color(0xFFC3C6FF) private val onSecondaryContainerDarkHighContrast = Color(0xFF000000) private val tertiaryDarkHighContrast = Color(0xFFFFF9FA) private val onTertiaryDarkHighContrast = Color(0xFF000000) private val tertiaryContainerDarkHighContrast = Color(0xFFF7B3FF) private val onTertiaryContainerDarkHighContrast = Color(0xFF000000) private val errorDarkHighContrast = Color(0xFFFFF9F9) private val onErrorDarkHighContrast = Color(0xFF000000) private val errorContainerDarkHighContrast = Color(0xFFFFBAB1) private val onErrorContainerDarkHighContrast = Color(0xFF000000) private val backgroundDarkHighContrast = Color(0xFF12131D) private val onBackgroundDarkHighContrast = Color(0xFFE3E1F0) private val surfaceDarkHighContrast = Color(0xFF12131D) private val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) private val surfaceVariantDarkHighContrast = Color(0xFF454557) private val onSurfaceVariantDarkHighContrast = Color(0xFFFDF9FF) private val outlineDarkHighContrast = Color(0xFFCAC9DE) private val outlineVariantDarkHighContrast = Color(0xFFCAC9DE) private val scrimDarkHighContrast = Color(0xFF000000) private val inverseSurfaceDarkHighContrast = Color(0xFFE3E1F0) private val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) private val inversePrimaryDarkHighContrast = Color(0xFF000497) private val surfaceDimDarkHighContrast = Color(0xFF12131D) private val surfaceBrightDarkHighContrast = Color(0xFF383844) private val surfaceContainerLowestDarkHighContrast = Color(0xFF0D0D17) private val surfaceContainerLowDarkHighContrast = Color(0xFF1A1B25) private val surfaceContainerDarkHighContrast = Color(0xFF1E1F29) private val surfaceContainerHighDarkHighContrast = Color(0xFF292934) private val surfaceContainerHighestDarkHighContrast = Color(0xFF34343F) private val sereneLightScheme = lightColorScheme( primary = primaryLight, onPrimary = onPrimaryLight, primaryContainer = primaryContainerLight, onPrimaryContainer = onPrimaryContainerLight, secondary = secondaryLight, onSecondary = onSecondaryLight, secondaryContainer = secondaryContainerLight, onSecondaryContainer = onSecondaryContainerLight, tertiary = tertiaryLight, onTertiary = onTertiaryLight, tertiaryContainer = tertiaryContainerLight, onTertiaryContainer = onTertiaryContainerLight, error = errorLight, onError = onErrorLight, errorContainer = errorContainerLight, onErrorContainer = onErrorContainerLight, background = backgroundLight, onBackground = onBackgroundLight, surface = surfaceLight, onSurface = onSurfaceLight, surfaceVariant = surfaceVariantLight, onSurfaceVariant = onSurfaceVariantLight, outline = outlineLight, outlineVariant = outlineVariantLight, scrim = scrimLight, inverseSurface = inverseSurfaceLight, inverseOnSurface = inverseOnSurfaceLight, inversePrimary = inversePrimaryLight, surfaceDim = surfaceDimLight, surfaceBright = surfaceBrightLight, surfaceContainerLowest = surfaceContainerLowestLight, surfaceContainerLow = surfaceContainerLowLight, surfaceContainer = surfaceContainerLight, surfaceContainerHigh = surfaceContainerHighLight, surfaceContainerHighest = surfaceContainerHighestLight, ) private val sereneDarkScheme = darkColorScheme( primary = primaryDark, onPrimary = onPrimaryDark, primaryContainer = primaryContainerDark, onPrimaryContainer = onPrimaryContainerDark, secondary = secondaryDark, onSecondary = onSecondaryDark, secondaryContainer = secondaryContainerDark, onSecondaryContainer = onSecondaryContainerDark, tertiary = tertiaryDark, onTertiary = onTertiaryDark, tertiaryContainer = tertiaryContainerDark, onTertiaryContainer = onTertiaryContainerDark, error = errorDark, onError = onErrorDark, errorContainer = errorContainerDark, onErrorContainer = onErrorContainerDark, background = backgroundDark, onBackground = onBackgroundDark, surface = surfaceDark, onSurface = onSurfaceDark, surfaceVariant = surfaceVariantDark, onSurfaceVariant = onSurfaceVariantDark, outline = outlineDark, outlineVariant = outlineVariantDark, scrim = scrimDark, inverseSurface = inverseSurfaceDark, inverseOnSurface = inverseOnSurfaceDark, inversePrimary = inversePrimaryDark, surfaceDim = surfaceDimDark, surfaceBright = surfaceBrightDark, surfaceContainerLowest = surfaceContainerLowestDark, surfaceContainerLow = surfaceContainerLowDark, surfaceContainer = surfaceContainerDark, surfaceContainerHigh = surfaceContainerHighDark, surfaceContainerHighest = surfaceContainerHighestDark, ) private val sereneMediumContrastLightColorScheme = lightColorScheme( primary = primaryLightMediumContrast, onPrimary = onPrimaryLightMediumContrast, primaryContainer = primaryContainerLightMediumContrast, onPrimaryContainer = onPrimaryContainerLightMediumContrast, secondary = secondaryLightMediumContrast, onSecondary = onSecondaryLightMediumContrast, secondaryContainer = secondaryContainerLightMediumContrast, onSecondaryContainer = onSecondaryContainerLightMediumContrast, tertiary = tertiaryLightMediumContrast, onTertiary = onTertiaryLightMediumContrast, tertiaryContainer = tertiaryContainerLightMediumContrast, onTertiaryContainer = onTertiaryContainerLightMediumContrast, error = errorLightMediumContrast, onError = onErrorLightMediumContrast, errorContainer = errorContainerLightMediumContrast, onErrorContainer = onErrorContainerLightMediumContrast, background = backgroundLightMediumContrast, onBackground = onBackgroundLightMediumContrast, surface = surfaceLightMediumContrast, onSurface = onSurfaceLightMediumContrast, surfaceVariant = surfaceVariantLightMediumContrast, onSurfaceVariant = onSurfaceVariantLightMediumContrast, outline = outlineLightMediumContrast, outlineVariant = outlineVariantLightMediumContrast, scrim = scrimLightMediumContrast, inverseSurface = inverseSurfaceLightMediumContrast, inverseOnSurface = inverseOnSurfaceLightMediumContrast, inversePrimary = inversePrimaryLightMediumContrast, surfaceDim = surfaceDimLightMediumContrast, surfaceBright = surfaceBrightLightMediumContrast, surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, surfaceContainerLow = surfaceContainerLowLightMediumContrast, surfaceContainer = surfaceContainerLightMediumContrast, surfaceContainerHigh = surfaceContainerHighLightMediumContrast, surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, ) private val sereneHighContrastLightColorScheme = lightColorScheme( primary = primaryLightHighContrast, onPrimary = onPrimaryLightHighContrast, primaryContainer = primaryContainerLightHighContrast, onPrimaryContainer = onPrimaryContainerLightHighContrast, secondary = secondaryLightHighContrast, onSecondary = onSecondaryLightHighContrast, secondaryContainer = secondaryContainerLightHighContrast, onSecondaryContainer = onSecondaryContainerLightHighContrast, tertiary = tertiaryLightHighContrast, onTertiary = onTertiaryLightHighContrast, tertiaryContainer = tertiaryContainerLightHighContrast, onTertiaryContainer = onTertiaryContainerLightHighContrast, error = errorLightHighContrast, onError = onErrorLightHighContrast, errorContainer = errorContainerLightHighContrast, onErrorContainer = onErrorContainerLightHighContrast, background = backgroundLightHighContrast, onBackground = onBackgroundLightHighContrast, surface = surfaceLightHighContrast, onSurface = onSurfaceLightHighContrast, surfaceVariant = surfaceVariantLightHighContrast, onSurfaceVariant = onSurfaceVariantLightHighContrast, outline = outlineLightHighContrast, outlineVariant = outlineVariantLightHighContrast, scrim = scrimLightHighContrast, inverseSurface = inverseSurfaceLightHighContrast, inverseOnSurface = inverseOnSurfaceLightHighContrast, inversePrimary = inversePrimaryLightHighContrast, surfaceDim = surfaceDimLightHighContrast, surfaceBright = surfaceBrightLightHighContrast, surfaceContainerLowest = surfaceContainerLowestLightHighContrast, surfaceContainerLow = surfaceContainerLowLightHighContrast, surfaceContainer = surfaceContainerLightHighContrast, surfaceContainerHigh = surfaceContainerHighLightHighContrast, surfaceContainerHighest = surfaceContainerHighestLightHighContrast, ) private val sereneMediumContrastDarkColorScheme = darkColorScheme( primary = primaryDarkMediumContrast, onPrimary = onPrimaryDarkMediumContrast, primaryContainer = primaryContainerDarkMediumContrast, onPrimaryContainer = onPrimaryContainerDarkMediumContrast, secondary = secondaryDarkMediumContrast, onSecondary = onSecondaryDarkMediumContrast, secondaryContainer = secondaryContainerDarkMediumContrast, onSecondaryContainer = onSecondaryContainerDarkMediumContrast, tertiary = tertiaryDarkMediumContrast, onTertiary = onTertiaryDarkMediumContrast, tertiaryContainer = tertiaryContainerDarkMediumContrast, onTertiaryContainer = onTertiaryContainerDarkMediumContrast, error = errorDarkMediumContrast, onError = onErrorDarkMediumContrast, errorContainer = errorContainerDarkMediumContrast, onErrorContainer = onErrorContainerDarkMediumContrast, background = backgroundDarkMediumContrast, onBackground = onBackgroundDarkMediumContrast, surface = surfaceDarkMediumContrast, onSurface = onSurfaceDarkMediumContrast, surfaceVariant = surfaceVariantDarkMediumContrast, onSurfaceVariant = onSurfaceVariantDarkMediumContrast, outline = outlineDarkMediumContrast, outlineVariant = outlineVariantDarkMediumContrast, scrim = scrimDarkMediumContrast, inverseSurface = inverseSurfaceDarkMediumContrast, inverseOnSurface = inverseOnSurfaceDarkMediumContrast, inversePrimary = inversePrimaryDarkMediumContrast, surfaceDim = surfaceDimDarkMediumContrast, surfaceBright = surfaceBrightDarkMediumContrast, surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, surfaceContainerLow = surfaceContainerLowDarkMediumContrast, surfaceContainer = surfaceContainerDarkMediumContrast, surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, ) private val sereneHighContrastDarkColorScheme = darkColorScheme( primary = primaryDarkHighContrast, onPrimary = onPrimaryDarkHighContrast, primaryContainer = primaryContainerDarkHighContrast, onPrimaryContainer = onPrimaryContainerDarkHighContrast, secondary = secondaryDarkHighContrast, onSecondary = onSecondaryDarkHighContrast, secondaryContainer = secondaryContainerDarkHighContrast, onSecondaryContainer = onSecondaryContainerDarkHighContrast, tertiary = tertiaryDarkHighContrast, onTertiary = onTertiaryDarkHighContrast, tertiaryContainer = tertiaryContainerDarkHighContrast, onTertiaryContainer = onTertiaryContainerDarkHighContrast, error = errorDarkHighContrast, onError = onErrorDarkHighContrast, errorContainer = errorContainerDarkHighContrast, onErrorContainer = onErrorContainerDarkHighContrast, background = backgroundDarkHighContrast, onBackground = onBackgroundDarkHighContrast, surface = surfaceDarkHighContrast, onSurface = onSurfaceDarkHighContrast, surfaceVariant = surfaceVariantDarkHighContrast, onSurfaceVariant = onSurfaceVariantDarkHighContrast, outline = outlineDarkHighContrast, outlineVariant = outlineVariantDarkHighContrast, scrim = scrimDarkHighContrast, inverseSurface = inverseSurfaceDarkHighContrast, inverseOnSurface = inverseOnSurfaceDarkHighContrast, inversePrimary = inversePrimaryDarkHighContrast, surfaceDim = surfaceDimDarkHighContrast, surfaceBright = surfaceBrightDarkHighContrast, surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, surfaceContainerLow = surfaceContainerLowDarkHighContrast, surfaceContainer = surfaceContainerDarkHighContrast, surfaceContainerHigh = surfaceContainerHighDarkHighContrast, surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, ) val sereneColorSchemes = ColorSchemes( light = sereneLightScheme, dark = sereneDarkScheme, mediumContrastLight = sereneMediumContrastLightColorScheme, mediumContrastDark = sereneMediumContrastDarkColorScheme, highContrastLight = sereneHighContrastLightColorScheme, highContrastDark = sereneHighContrastDarkColorScheme, nameRes = R.string.label_color_scheme_serene, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/theme/colorSchemes/Twilight.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.theme.colorSchemes import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.ui.graphics.Color import com.looker.kenko.R // Purple & Orange private val primaryLight = Color(0xFF7900A9) private val onPrimaryLight = Color(0xFFFFFFFF) private val primaryContainerLight = Color(0xFFA92CE2) private val onPrimaryContainerLight = Color(0xFFFFFFFF) private val secondaryLight = Color(0xFF80459B) private val onSecondaryLight = Color(0xFFFFFFFF) private val secondaryContainerLight = Color(0xFFE7ABFF) private val onSecondaryContainerLight = Color(0xFF4F146B) private val tertiaryLight = Color(0xFFA40022) private val onTertiaryLight = Color(0xFFFFFFFF) private val tertiaryContainerLight = Color(0xFFE0273C) private val onTertiaryContainerLight = Color(0xFFFFFFFF) private val errorLight = Color(0xFFBA1A1A) private val onErrorLight = Color(0xFFFFFFFF) private val errorContainerLight = Color(0xFFFFDAD6) private val onErrorContainerLight = Color(0xFF410002) private val backgroundLight = Color(0xFFFFF7FB) private val onBackgroundLight = Color(0xFF211923) private val surfaceLight = Color(0xFFFFF7FB) private val onSurfaceLight = Color(0xFF211923) private val surfaceVariantLight = Color(0xFFF0DCF2) private val onSurfaceVariantLight = Color(0xFF4F4253) private val outlineLight = Color(0xFF817285) private val outlineVariantLight = Color(0xFFD3C1D5) private val scrimLight = Color(0xFF000000) private val inverseSurfaceLight = Color(0xFF362D38) private val inverseOnSurfaceLight = Color(0xFFFBECFB) private val inversePrimaryLight = Color(0xFFEAB2FF) private val surfaceDimLight = Color(0xFFE4D6E4) private val surfaceBrightLight = Color(0xFFFFF7FB) private val surfaceContainerLowestLight = Color(0xFFFFFFFF) private val surfaceContainerLowLight = Color(0xFFFEEFFE) private val surfaceContainerLight = Color(0xFFF8E9F8) private val surfaceContainerHighLight = Color(0xFFF2E4F2) private val surfaceContainerHighestLight = Color(0xFFEDDEED) private val primaryLightMediumContrast = Color(0xFF6D0099) private val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) private val primaryContainerLightMediumContrast = Color(0xFFA92CE2) private val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) private val secondaryLightMediumContrast = Color(0xFF62287D) private val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) private val secondaryContainerLightMediumContrast = Color(0xFF985CB3) private val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) private val tertiaryLightMediumContrast = Color(0xFF8B001B) private val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) private val tertiaryContainerLightMediumContrast = Color(0xFFE0273C) private val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) private val errorLightMediumContrast = Color(0xFF8C0009) private val onErrorLightMediumContrast = Color(0xFFFFFFFF) private val errorContainerLightMediumContrast = Color(0xFFDA342E) private val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) private val backgroundLightMediumContrast = Color(0xFFFFF7FB) private val onBackgroundLightMediumContrast = Color(0xFF211923) private val surfaceLightMediumContrast = Color(0xFFFFF7FB) private val onSurfaceLightMediumContrast = Color(0xFF211923) private val surfaceVariantLightMediumContrast = Color(0xFFF0DCF2) private val onSurfaceVariantLightMediumContrast = Color(0xFF4B3E4F) private val outlineLightMediumContrast = Color(0xFF685A6C) private val outlineVariantLightMediumContrast = Color(0xFF857688) private val scrimLightMediumContrast = Color(0xFF000000) private val inverseSurfaceLightMediumContrast = Color(0xFF362D38) private val inverseOnSurfaceLightMediumContrast = Color(0xFFFBECFB) private val inversePrimaryLightMediumContrast = Color(0xFFEAB2FF) private val surfaceDimLightMediumContrast = Color(0xFFE4D6E4) private val surfaceBrightLightMediumContrast = Color(0xFFFFF7FB) private val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) private val surfaceContainerLowLightMediumContrast = Color(0xFFFEEFFE) private val surfaceContainerLightMediumContrast = Color(0xFFF8E9F8) private val surfaceContainerHighLightMediumContrast = Color(0xFFF2E4F2) private val surfaceContainerHighestLightMediumContrast = Color(0xFFEDDEED) private val primaryLightHighContrast = Color(0xFF3C0055) private val onPrimaryLightHighContrast = Color(0xFFFFFFFF) private val primaryContainerLightHighContrast = Color(0xFF6D0099) private val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) private val secondaryLightHighContrast = Color(0xFF3C0055) private val onSecondaryLightHighContrast = Color(0xFFFFFFFF) private val secondaryContainerLightHighContrast = Color(0xFF62287D) private val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) private val tertiaryLightHighContrast = Color(0xFF4D000A) private val onTertiaryLightHighContrast = Color(0xFFFFFFFF) private val tertiaryContainerLightHighContrast = Color(0xFF8B001B) private val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) private val errorLightHighContrast = Color(0xFF4E0002) private val onErrorLightHighContrast = Color(0xFFFFFFFF) private val errorContainerLightHighContrast = Color(0xFF8C0009) private val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) private val backgroundLightHighContrast = Color(0xFFFFF7FB) private val onBackgroundLightHighContrast = Color(0xFF211923) private val surfaceLightHighContrast = Color(0xFFFFF7FB) private val onSurfaceLightHighContrast = Color(0xFF000000) private val surfaceVariantLightHighContrast = Color(0xFFF0DCF2) private val onSurfaceVariantLightHighContrast = Color(0xFF2B202F) private val outlineLightHighContrast = Color(0xFF4B3E4F) private val outlineVariantLightHighContrast = Color(0xFF4B3E4F) private val scrimLightHighContrast = Color(0xFF000000) private val inverseSurfaceLightHighContrast = Color(0xFF362D38) private val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) private val inversePrimaryLightHighContrast = Color(0xFFFBE5FF) private val surfaceDimLightHighContrast = Color(0xFFE4D6E4) private val surfaceBrightLightHighContrast = Color(0xFFFFF7FB) private val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) private val surfaceContainerLowLightHighContrast = Color(0xFFFEEFFE) private val surfaceContainerLightHighContrast = Color(0xFFF8E9F8) private val surfaceContainerHighLightHighContrast = Color(0xFFF2E4F2) private val surfaceContainerHighestLightHighContrast = Color(0xFFEDDEED) private val primaryDark = Color(0xFFEAB2FF) private val onPrimaryDark = Color(0xFF510073) private val primaryContainerDark = Color(0xFF8E00C5) private val onPrimaryContainerDark = Color(0xFFFFFEFF) private val secondaryDark = Color(0xFFEAB2FF) private val onSecondaryDark = Color(0xFF4D1169) private val secondaryContainerDark = Color(0xFF5C2277) private val onSecondaryContainerDark = Color(0xFFEFC2FF) private val tertiaryDark = Color(0xFFFFB3B1) private val onTertiaryDark = Color(0xFF680012) private val tertiaryContainerDark = Color(0xFFE0273C) private val onTertiaryContainerDark = Color(0xFFFFFFFF) private val errorDark = Color(0xFFFFB4AB) private val onErrorDark = Color(0xFF690005) private val errorContainerDark = Color(0xFF93000A) private val onErrorContainerDark = Color(0xFFFFDAD6) private val backgroundDark = Color(0xFF18111B) private val onBackgroundDark = Color(0xFFEDDEED) private val surfaceDark = Color(0xFF18111B) private val onSurfaceDark = Color(0xFFEDDEED) private val surfaceVariantDark = Color(0xFF4F4253) private val onSurfaceVariantDark = Color(0xFFD3C1D5) private val outlineDark = Color(0xFF9C8B9F) private val outlineVariantDark = Color(0xFF4F4253) private val scrimDark = Color(0xFF000000) private val inverseSurfaceDark = Color(0xFFEDDEED) private val inverseOnSurfaceDark = Color(0xFF362D38) private val inversePrimaryDark = Color(0xFF9708D0) private val surfaceDimDark = Color(0xFF18111B) private val surfaceBrightDark = Color(0xFF3F3641) private val surfaceContainerLowestDark = Color(0xFF130C15) private val surfaceContainerLowDark = Color(0xFF211923) private val surfaceContainerDark = Color(0xFF251D27) private val surfaceContainerHighDark = Color(0xFF2F2732) private val surfaceContainerHighestDark = Color(0xFF3B323D) private val primaryDarkMediumContrast = Color(0xFFECB8FF) private val onPrimaryDarkMediumContrast = Color(0xFF29003D) private val primaryContainerDarkMediumContrast = Color(0xFFCC5FFF) private val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) private val secondaryDarkMediumContrast = Color(0xFFECB8FF) private val onSecondaryDarkMediumContrast = Color(0xFF29003D) private val secondaryContainerDarkMediumContrast = Color(0xFFB778D2) private val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) private val tertiaryDarkMediumContrast = Color(0xFFFFB9B7) private val onTertiaryDarkMediumContrast = Color(0xFF370005) private val tertiaryContainerDarkMediumContrast = Color(0xFFFF525B) private val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) private val errorDarkMediumContrast = Color(0xFFFFBAB1) private val onErrorDarkMediumContrast = Color(0xFF370001) private val errorContainerDarkMediumContrast = Color(0xFFFF5449) private val onErrorContainerDarkMediumContrast = Color(0xFF000000) private val backgroundDarkMediumContrast = Color(0xFF18111B) private val onBackgroundDarkMediumContrast = Color(0xFFEDDEED) private val surfaceDarkMediumContrast = Color(0xFF18111B) private val onSurfaceDarkMediumContrast = Color(0xFFFFF9FB) private val surfaceVariantDarkMediumContrast = Color(0xFF4F4253) private val onSurfaceVariantDarkMediumContrast = Color(0xFFD7C5DA) private val outlineDarkMediumContrast = Color(0xFFAE9DB1) private val outlineVariantDarkMediumContrast = Color(0xFF8E7E91) private val scrimDarkMediumContrast = Color(0xFF000000) private val inverseSurfaceDarkMediumContrast = Color(0xFFEDDEED) private val inverseOnSurfaceDarkMediumContrast = Color(0xFF2F2732) private val inversePrimaryDarkMediumContrast = Color(0xFF7500A3) private val surfaceDimDarkMediumContrast = Color(0xFF18111B) private val surfaceBrightDarkMediumContrast = Color(0xFF3F3641) private val surfaceContainerLowestDarkMediumContrast = Color(0xFF130C15) private val surfaceContainerLowDarkMediumContrast = Color(0xFF211923) private val surfaceContainerDarkMediumContrast = Color(0xFF251D27) private val surfaceContainerHighDarkMediumContrast = Color(0xFF2F2732) private val surfaceContainerHighestDarkMediumContrast = Color(0xFF3B323D) private val primaryDarkHighContrast = Color(0xFFFFF9FB) private val onPrimaryDarkHighContrast = Color(0xFF000000) private val primaryContainerDarkHighContrast = Color(0xFFECB8FF) private val onPrimaryContainerDarkHighContrast = Color(0xFF000000) private val secondaryDarkHighContrast = Color(0xFFFFF9FB) private val onSecondaryDarkHighContrast = Color(0xFF000000) private val secondaryContainerDarkHighContrast = Color(0xFFECB8FF) private val onSecondaryContainerDarkHighContrast = Color(0xFF000000) private val tertiaryDarkHighContrast = Color(0xFFFFF9F9) private val onTertiaryDarkHighContrast = Color(0xFF000000) private val tertiaryContainerDarkHighContrast = Color(0xFFFFB9B7) private val onTertiaryContainerDarkHighContrast = Color(0xFF000000) private val errorDarkHighContrast = Color(0xFFFFF9F9) private val onErrorDarkHighContrast = Color(0xFF000000) private val errorContainerDarkHighContrast = Color(0xFFFFBAB1) private val onErrorContainerDarkHighContrast = Color(0xFF000000) private val backgroundDarkHighContrast = Color(0xFF18111B) private val onBackgroundDarkHighContrast = Color(0xFFEDDEED) private val surfaceDarkHighContrast = Color(0xFF18111B) private val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) private val surfaceVariantDarkHighContrast = Color(0xFF4F4253) private val onSurfaceVariantDarkHighContrast = Color(0xFFFFF9FB) private val outlineDarkHighContrast = Color(0xFFD7C5DA) private val outlineVariantDarkHighContrast = Color(0xFFD7C5DA) private val scrimDarkHighContrast = Color(0xFF000000) private val inverseSurfaceDarkHighContrast = Color(0xFFEDDEED) private val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) private val inversePrimaryDarkHighContrast = Color(0xFF470065) private val surfaceDimDarkHighContrast = Color(0xFF18111B) private val surfaceBrightDarkHighContrast = Color(0xFF3F3641) private val surfaceContainerLowestDarkHighContrast = Color(0xFF130C15) private val surfaceContainerLowDarkHighContrast = Color(0xFF211923) private val surfaceContainerDarkHighContrast = Color(0xFF251D27) private val surfaceContainerHighDarkHighContrast = Color(0xFF2F2732) private val surfaceContainerHighestDarkHighContrast = Color(0xFF3B323D) private val twilightLightScheme = lightColorScheme( primary = primaryLight, onPrimary = onPrimaryLight, primaryContainer = primaryContainerLight, onPrimaryContainer = onPrimaryContainerLight, secondary = secondaryLight, onSecondary = onSecondaryLight, secondaryContainer = secondaryContainerLight, onSecondaryContainer = onSecondaryContainerLight, tertiary = tertiaryLight, onTertiary = onTertiaryLight, tertiaryContainer = tertiaryContainerLight, onTertiaryContainer = onTertiaryContainerLight, error = errorLight, onError = onErrorLight, errorContainer = errorContainerLight, onErrorContainer = onErrorContainerLight, background = backgroundLight, onBackground = onBackgroundLight, surface = surfaceLight, onSurface = onSurfaceLight, surfaceVariant = surfaceVariantLight, onSurfaceVariant = onSurfaceVariantLight, outline = outlineLight, outlineVariant = outlineVariantLight, scrim = scrimLight, inverseSurface = inverseSurfaceLight, inverseOnSurface = inverseOnSurfaceLight, inversePrimary = inversePrimaryLight, surfaceDim = surfaceDimLight, surfaceBright = surfaceBrightLight, surfaceContainerLowest = surfaceContainerLowestLight, surfaceContainerLow = surfaceContainerLowLight, surfaceContainer = surfaceContainerLight, surfaceContainerHigh = surfaceContainerHighLight, surfaceContainerHighest = surfaceContainerHighestLight, ) private val twilightDarkScheme = darkColorScheme( primary = primaryDark, onPrimary = onPrimaryDark, primaryContainer = primaryContainerDark, onPrimaryContainer = onPrimaryContainerDark, secondary = secondaryDark, onSecondary = onSecondaryDark, secondaryContainer = secondaryContainerDark, onSecondaryContainer = onSecondaryContainerDark, tertiary = tertiaryDark, onTertiary = onTertiaryDark, tertiaryContainer = tertiaryContainerDark, onTertiaryContainer = onTertiaryContainerDark, error = errorDark, onError = onErrorDark, errorContainer = errorContainerDark, onErrorContainer = onErrorContainerDark, background = backgroundDark, onBackground = onBackgroundDark, surface = surfaceDark, onSurface = onSurfaceDark, surfaceVariant = surfaceVariantDark, onSurfaceVariant = onSurfaceVariantDark, outline = outlineDark, outlineVariant = outlineVariantDark, scrim = scrimDark, inverseSurface = inverseSurfaceDark, inverseOnSurface = inverseOnSurfaceDark, inversePrimary = inversePrimaryDark, surfaceDim = surfaceDimDark, surfaceBright = surfaceBrightDark, surfaceContainerLowest = surfaceContainerLowestDark, surfaceContainerLow = surfaceContainerLowDark, surfaceContainer = surfaceContainerDark, surfaceContainerHigh = surfaceContainerHighDark, surfaceContainerHighest = surfaceContainerHighestDark, ) private val twilightMediumContrastLightColorScheme = lightColorScheme( primary = primaryLightMediumContrast, onPrimary = onPrimaryLightMediumContrast, primaryContainer = primaryContainerLightMediumContrast, onPrimaryContainer = onPrimaryContainerLightMediumContrast, secondary = secondaryLightMediumContrast, onSecondary = onSecondaryLightMediumContrast, secondaryContainer = secondaryContainerLightMediumContrast, onSecondaryContainer = onSecondaryContainerLightMediumContrast, tertiary = tertiaryLightMediumContrast, onTertiary = onTertiaryLightMediumContrast, tertiaryContainer = tertiaryContainerLightMediumContrast, onTertiaryContainer = onTertiaryContainerLightMediumContrast, error = errorLightMediumContrast, onError = onErrorLightMediumContrast, errorContainer = errorContainerLightMediumContrast, onErrorContainer = onErrorContainerLightMediumContrast, background = backgroundLightMediumContrast, onBackground = onBackgroundLightMediumContrast, surface = surfaceLightMediumContrast, onSurface = onSurfaceLightMediumContrast, surfaceVariant = surfaceVariantLightMediumContrast, onSurfaceVariant = onSurfaceVariantLightMediumContrast, outline = outlineLightMediumContrast, outlineVariant = outlineVariantLightMediumContrast, scrim = scrimLightMediumContrast, inverseSurface = inverseSurfaceLightMediumContrast, inverseOnSurface = inverseOnSurfaceLightMediumContrast, inversePrimary = inversePrimaryLightMediumContrast, surfaceDim = surfaceDimLightMediumContrast, surfaceBright = surfaceBrightLightMediumContrast, surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, surfaceContainerLow = surfaceContainerLowLightMediumContrast, surfaceContainer = surfaceContainerLightMediumContrast, surfaceContainerHigh = surfaceContainerHighLightMediumContrast, surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, ) private val twilightHighContrastLightColorScheme = lightColorScheme( primary = primaryLightHighContrast, onPrimary = onPrimaryLightHighContrast, primaryContainer = primaryContainerLightHighContrast, onPrimaryContainer = onPrimaryContainerLightHighContrast, secondary = secondaryLightHighContrast, onSecondary = onSecondaryLightHighContrast, secondaryContainer = secondaryContainerLightHighContrast, onSecondaryContainer = onSecondaryContainerLightHighContrast, tertiary = tertiaryLightHighContrast, onTertiary = onTertiaryLightHighContrast, tertiaryContainer = tertiaryContainerLightHighContrast, onTertiaryContainer = onTertiaryContainerLightHighContrast, error = errorLightHighContrast, onError = onErrorLightHighContrast, errorContainer = errorContainerLightHighContrast, onErrorContainer = onErrorContainerLightHighContrast, background = backgroundLightHighContrast, onBackground = onBackgroundLightHighContrast, surface = surfaceLightHighContrast, onSurface = onSurfaceLightHighContrast, surfaceVariant = surfaceVariantLightHighContrast, onSurfaceVariant = onSurfaceVariantLightHighContrast, outline = outlineLightHighContrast, outlineVariant = outlineVariantLightHighContrast, scrim = scrimLightHighContrast, inverseSurface = inverseSurfaceLightHighContrast, inverseOnSurface = inverseOnSurfaceLightHighContrast, inversePrimary = inversePrimaryLightHighContrast, surfaceDim = surfaceDimLightHighContrast, surfaceBright = surfaceBrightLightHighContrast, surfaceContainerLowest = surfaceContainerLowestLightHighContrast, surfaceContainerLow = surfaceContainerLowLightHighContrast, surfaceContainer = surfaceContainerLightHighContrast, surfaceContainerHigh = surfaceContainerHighLightHighContrast, surfaceContainerHighest = surfaceContainerHighestLightHighContrast, ) private val twilightMediumContrastDarkColorScheme = darkColorScheme( primary = primaryDarkMediumContrast, onPrimary = onPrimaryDarkMediumContrast, primaryContainer = primaryContainerDarkMediumContrast, onPrimaryContainer = onPrimaryContainerDarkMediumContrast, secondary = secondaryDarkMediumContrast, onSecondary = onSecondaryDarkMediumContrast, secondaryContainer = secondaryContainerDarkMediumContrast, onSecondaryContainer = onSecondaryContainerDarkMediumContrast, tertiary = tertiaryDarkMediumContrast, onTertiary = onTertiaryDarkMediumContrast, tertiaryContainer = tertiaryContainerDarkMediumContrast, onTertiaryContainer = onTertiaryContainerDarkMediumContrast, error = errorDarkMediumContrast, onError = onErrorDarkMediumContrast, errorContainer = errorContainerDarkMediumContrast, onErrorContainer = onErrorContainerDarkMediumContrast, background = backgroundDarkMediumContrast, onBackground = onBackgroundDarkMediumContrast, surface = surfaceDarkMediumContrast, onSurface = onSurfaceDarkMediumContrast, surfaceVariant = surfaceVariantDarkMediumContrast, onSurfaceVariant = onSurfaceVariantDarkMediumContrast, outline = outlineDarkMediumContrast, outlineVariant = outlineVariantDarkMediumContrast, scrim = scrimDarkMediumContrast, inverseSurface = inverseSurfaceDarkMediumContrast, inverseOnSurface = inverseOnSurfaceDarkMediumContrast, inversePrimary = inversePrimaryDarkMediumContrast, surfaceDim = surfaceDimDarkMediumContrast, surfaceBright = surfaceBrightDarkMediumContrast, surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, surfaceContainerLow = surfaceContainerLowDarkMediumContrast, surfaceContainer = surfaceContainerDarkMediumContrast, surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, ) private val twilightHighContrastDarkColorScheme = darkColorScheme( primary = primaryDarkHighContrast, onPrimary = onPrimaryDarkHighContrast, primaryContainer = primaryContainerDarkHighContrast, onPrimaryContainer = onPrimaryContainerDarkHighContrast, secondary = secondaryDarkHighContrast, onSecondary = onSecondaryDarkHighContrast, secondaryContainer = secondaryContainerDarkHighContrast, onSecondaryContainer = onSecondaryContainerDarkHighContrast, tertiary = tertiaryDarkHighContrast, onTertiary = onTertiaryDarkHighContrast, tertiaryContainer = tertiaryContainerDarkHighContrast, onTertiaryContainer = onTertiaryContainerDarkHighContrast, error = errorDarkHighContrast, onError = onErrorDarkHighContrast, errorContainer = errorContainerDarkHighContrast, onErrorContainer = onErrorContainerDarkHighContrast, background = backgroundDarkHighContrast, onBackground = onBackgroundDarkHighContrast, surface = surfaceDarkHighContrast, onSurface = onSurfaceDarkHighContrast, surfaceVariant = surfaceVariantDarkHighContrast, onSurfaceVariant = onSurfaceVariantDarkHighContrast, outline = outlineDarkHighContrast, outlineVariant = outlineVariantDarkHighContrast, scrim = scrimDarkHighContrast, inverseSurface = inverseSurfaceDarkHighContrast, inverseOnSurface = inverseOnSurfaceDarkHighContrast, inversePrimary = inversePrimaryDarkHighContrast, surfaceDim = surfaceDimDarkHighContrast, surfaceBright = surfaceBrightDarkHighContrast, surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, surfaceContainerLow = surfaceContainerLowDarkHighContrast, surfaceContainer = surfaceContainerDarkHighContrast, surfaceContainerHigh = surfaceContainerHighDarkHighContrast, surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, ) val twilightColorSchemes = ColorSchemes( light = twilightLightScheme, dark = twilightDarkScheme, mediumContrastLight = twilightMediumContrastLightColorScheme, mediumContrastDark = twilightMediumContrastDarkColorScheme, highContrastLight = twilightHighContrastLightColorScheme, highContrastDark = twilightHighContrastDarkColorScheme, nameRes = R.string.label_color_scheme_twilight, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/ui/theme/colorSchemes/Zestful.kt ================================================ /* * 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 . */ package com.looker.kenko.ui.theme.colorSchemes import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.ui.graphics.Color import com.looker.kenko.R // Lime & Purple private val primaryLight = Color(0xFF63611E) private val onPrimaryLight = Color(0xFFFFFFFF) private val primaryContainerLight = Color(0xFFF7F3A1) private val onPrimaryContainerLight = Color(0xFF545210) private val secondaryLight = Color(0xFF62603E) private val onSecondaryLight = Color(0xFFFFFFFF) private val secondaryContainerLight = Color(0xFFF5F1C6) private val onSecondaryContainerLight = Color(0xFF525131) private val tertiaryLight = Color(0xFF70576B) private val onTertiaryLight = Color(0xFFFFFFFF) private val tertiaryContainerLight = Color(0xFFE5C4DC) private val onTertiaryContainerLight = Color(0xFF4A3447) private val errorLight = Color(0xFFBA1A1A) private val onErrorLight = Color(0xFFFFFFFF) private val errorContainerLight = Color(0xFFFFDAD6) private val onErrorContainerLight = Color(0xFF410002) private val backgroundLight = Color(0xFFFDF9F0) private val onBackgroundLight = Color(0xFF1C1C16) private val surfaceLight = Color(0xFFFDF9F0) private val onSurfaceLight = Color(0xFF1C1C16) private val surfaceVariantLight = Color(0xFFE7E3D0) private val onSurfaceVariantLight = Color(0xFF49473A) private val outlineLight = Color(0xFF7A7768) private val outlineVariantLight = Color(0xFFCAC7B5) private val scrimLight = Color(0xFF000000) private val inverseSurfaceLight = Color(0xFF31302A) private val inverseOnSurfaceLight = Color(0xFFF4F0E7) private val inversePrimaryLight = Color(0xFFCDCA7C) private val surfaceDimLight = Color(0xFFDDDAD1) private val surfaceBrightLight = Color(0xFFFDF9F0) private val surfaceContainerLowestLight = Color(0xFFFFFFFF) private val surfaceContainerLowLight = Color(0xFFF7F3EA) private val surfaceContainerLight = Color(0xFFF2EEE4) private val surfaceContainerHighLight = Color(0xFFECE8DF) private val surfaceContainerHighestLight = Color(0xFFE6E2D9) private val primaryLightMediumContrast = Color(0xFF474502) private val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) private val primaryContainerLightMediumContrast = Color(0xFF7A7733) private val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) private val secondaryLightMediumContrast = Color(0xFF464425) private val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) private val secondaryContainerLightMediumContrast = Color(0xFF787653) private val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) private val tertiaryLightMediumContrast = Color(0xFF533B4F) private val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) private val tertiaryContainerLightMediumContrast = Color(0xFF876C82) private val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) private val errorLightMediumContrast = Color(0xFF8C0009) private val onErrorLightMediumContrast = Color(0xFFFFFFFF) private val errorContainerLightMediumContrast = Color(0xFFDA342E) private val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) private val backgroundLightMediumContrast = Color(0xFFFDF9F0) private val onBackgroundLightMediumContrast = Color(0xFF1C1C16) private val surfaceLightMediumContrast = Color(0xFFFDF9F0) private val onSurfaceLightMediumContrast = Color(0xFF1C1C16) private val surfaceVariantLightMediumContrast = Color(0xFFE7E3D0) private val onSurfaceVariantLightMediumContrast = Color(0xFF454336) private val outlineLightMediumContrast = Color(0xFF616051) private val outlineVariantLightMediumContrast = Color(0xFF7D7B6C) private val scrimLightMediumContrast = Color(0xFF000000) private val inverseSurfaceLightMediumContrast = Color(0xFF31302A) private val inverseOnSurfaceLightMediumContrast = Color(0xFFF4F0E7) private val inversePrimaryLightMediumContrast = Color(0xFFCDCA7C) private val surfaceDimLightMediumContrast = Color(0xFFDDDAD1) private val surfaceBrightLightMediumContrast = Color(0xFFFDF9F0) private val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) private val surfaceContainerLowLightMediumContrast = Color(0xFFF7F3EA) private val surfaceContainerLightMediumContrast = Color(0xFFF2EEE4) private val surfaceContainerHighLightMediumContrast = Color(0xFFECE8DF) private val surfaceContainerHighestLightMediumContrast = Color(0xFFE6E2D9) private val primaryLightHighContrast = Color(0xFF252300) private val onPrimaryLightHighContrast = Color(0xFFFFFFFF) private val primaryContainerLightHighContrast = Color(0xFF474502) private val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) private val secondaryLightHighContrast = Color(0xFF242308) private val onSecondaryLightHighContrast = Color(0xFFFFFFFF) private val secondaryContainerLightHighContrast = Color(0xFF464425) private val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) private val tertiaryLightHighContrast = Color(0xFF301B2D) private val onTertiaryLightHighContrast = Color(0xFFFFFFFF) private val tertiaryContainerLightHighContrast = Color(0xFF533B4F) private val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) private val errorLightHighContrast = Color(0xFF4E0002) private val onErrorLightHighContrast = Color(0xFFFFFFFF) private val errorContainerLightHighContrast = Color(0xFF8C0009) private val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) private val backgroundLightHighContrast = Color(0xFFFDF9F0) private val onBackgroundLightHighContrast = Color(0xFF1C1C16) private val surfaceLightHighContrast = Color(0xFFFDF9F0) private val onSurfaceLightHighContrast = Color(0xFF000000) private val surfaceVariantLightHighContrast = Color(0xFFE7E3D0) private val onSurfaceVariantLightHighContrast = Color(0xFF252419) private val outlineLightHighContrast = Color(0xFF454336) private val outlineVariantLightHighContrast = Color(0xFF454336) private val scrimLightHighContrast = Color(0xFF000000) private val inverseSurfaceLightHighContrast = Color(0xFF31302A) private val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) private val inversePrimaryLightHighContrast = Color(0xFFF4F09E) private val surfaceDimLightHighContrast = Color(0xFFDDDAD1) private val surfaceBrightLightHighContrast = Color(0xFFFDF9F0) private val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) private val surfaceContainerLowLightHighContrast = Color(0xFFF7F3EA) private val surfaceContainerLightHighContrast = Color(0xFFF2EEE4) private val surfaceContainerHighLightHighContrast = Color(0xFFECE8DF) private val surfaceContainerHighestLightHighContrast = Color(0xFFE6E2D9) private val primaryDark = Color(0xFFCECA75) private val onPrimaryDark = Color(0xFF333200) private val primaryContainerDark = Color(0xFFDCD888) private val onPrimaryContainerDark = Color(0xFF434100) private val secondaryDark = Color(0xFFCBC8A4) private val onSecondaryDark = Color(0xFF333214) private val secondaryContainerDark = Color(0xFF49482C) private val onSecondaryContainerDark = Color(0xFFE8E4BE) private val tertiaryDark = Color(0xFFFFE4F7) private val onTertiaryDark = Color(0xFF3F293C) private val tertiaryContainerDark = Color(0xFFD8B8D0) private val onTertiaryContainerDark = Color(0xFF412C3E) private val errorDark = Color(0xFFFFB4AB) private val onErrorDark = Color(0xFF690005) private val errorContainerDark = Color(0xFF93000A) private val onErrorContainerDark = Color(0xFFFFDAD6) private val backgroundDark = Color(0xFF14140E) private val onBackgroundDark = Color(0xFFE6E2D9) private val surfaceDark = Color(0xFF14140E) private val onSurfaceDark = Color(0xFFE6E2D9) private val surfaceVariantDark = Color(0xFF49473A) private val onSurfaceVariantDark = Color(0xFFCAC7B5) private val outlineDark = Color(0xFF949181) private val outlineVariantDark = Color(0xFF49473A) private val scrimDark = Color(0xFF000000) private val inverseSurfaceDark = Color(0xFFE6E2D9) private val inverseOnSurfaceDark = Color(0xFF31302A) private val inversePrimaryDark = Color(0xFF63611E) private val surfaceDimDark = Color(0xFF14140E) private val surfaceBrightDark = Color(0xFF3A3933) private val surfaceContainerLowestDark = Color(0xFF0F0E09) private val surfaceContainerLowDark = Color(0xFF1C1C16) private val surfaceContainerDark = Color(0xFF20201A) private val surfaceContainerHighDark = Color(0xFF2B2A24) private val surfaceContainerHighestDark = Color(0xFF36352F) private val primaryDarkMediumContrast = Color(0xFFFFFFFF) private val onPrimaryDarkMediumContrast = Color(0xFF333200) private val primaryContainerDarkMediumContrast = Color(0xFFDCD888) private val onPrimaryContainerDarkMediumContrast = Color(0xFF222100) private val secondaryDarkMediumContrast = Color(0xFFFFFFFF) private val onSecondaryDarkMediumContrast = Color(0xFF333214) private val secondaryContainerDarkMediumContrast = Color(0xFFDAD6AC) private val onSecondaryContainerDarkMediumContrast = Color(0xFF212005) private val tertiaryDarkMediumContrast = Color(0xFFFFE4F7) private val onTertiaryDarkMediumContrast = Color(0xFF3F293C) private val tertiaryContainerDarkMediumContrast = Color(0xFFD8B8D0) private val onTertiaryContainerDarkMediumContrast = Color(0xFF140413) private val errorDarkMediumContrast = Color(0xFFFFBAB1) private val onErrorDarkMediumContrast = Color(0xFF370001) private val errorContainerDarkMediumContrast = Color(0xFFFF5449) private val onErrorContainerDarkMediumContrast = Color(0xFF000000) private val backgroundDarkMediumContrast = Color(0xFF14140E) private val onBackgroundDarkMediumContrast = Color(0xFFE6E2D9) private val surfaceDarkMediumContrast = Color(0xFF14140E) private val onSurfaceDarkMediumContrast = Color(0xFFFFFAF1) private val surfaceVariantDarkMediumContrast = Color(0xFF49473A) private val onSurfaceVariantDarkMediumContrast = Color(0xFFCECBB9) private val outlineDarkMediumContrast = Color(0xFFA6A393) private val outlineVariantDarkMediumContrast = Color(0xFF868474) private val scrimDarkMediumContrast = Color(0xFF000000) private val inverseSurfaceDarkMediumContrast = Color(0xFFE6E2D9) private val inverseOnSurfaceDarkMediumContrast = Color(0xFF2B2A24) private val inversePrimaryDarkMediumContrast = Color(0xFF4C4A07) private val surfaceDimDarkMediumContrast = Color(0xFF14140E) private val surfaceBrightDarkMediumContrast = Color(0xFF3A3933) private val surfaceContainerLowestDarkMediumContrast = Color(0xFF0F0E09) private val surfaceContainerLowDarkMediumContrast = Color(0xFF1C1C16) private val surfaceContainerDarkMediumContrast = Color(0xFF20201A) private val surfaceContainerHighDarkMediumContrast = Color(0xFF2B2A24) private val surfaceContainerHighestDarkMediumContrast = Color(0xFF36352F) private val primaryDarkHighContrast = Color(0xFFFFFFFF) private val onPrimaryDarkHighContrast = Color(0xFF000000) private val primaryContainerDarkHighContrast = Color(0xFFDCD888) private val onPrimaryContainerDarkHighContrast = Color(0xFF000000) private val secondaryDarkHighContrast = Color(0xFFFFFFFF) private val onSecondaryDarkHighContrast = Color(0xFF000000) private val secondaryContainerDarkHighContrast = Color(0xFFDAD6AC) private val onSecondaryContainerDarkHighContrast = Color(0xFF000000) private val tertiaryDarkHighContrast = Color(0xFFFFF9FA) private val onTertiaryDarkHighContrast = Color(0xFF000000) private val tertiaryContainerDarkHighContrast = Color(0xFFE2C1D9) private val onTertiaryContainerDarkHighContrast = Color(0xFF000000) private val errorDarkHighContrast = Color(0xFFFFF9F9) private val onErrorDarkHighContrast = Color(0xFF000000) private val errorContainerDarkHighContrast = Color(0xFFFFBAB1) private val onErrorContainerDarkHighContrast = Color(0xFF000000) private val backgroundDarkHighContrast = Color(0xFF14140E) private val onBackgroundDarkHighContrast = Color(0xFFE6E2D9) private val surfaceDarkHighContrast = Color(0xFF14140E) private val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) private val surfaceVariantDarkHighContrast = Color(0xFF49473A) private val onSurfaceVariantDarkHighContrast = Color(0xFFFFFBEA) private val outlineDarkHighContrast = Color(0xFFCECBB9) private val outlineVariantDarkHighContrast = Color(0xFFCECBB9) private val scrimDarkHighContrast = Color(0xFF000000) private val inverseSurfaceDarkHighContrast = Color(0xFFE6E2D9) private val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) private val inversePrimaryDarkHighContrast = Color(0xFF2D2B00) private val surfaceDimDarkHighContrast = Color(0xFF14140E) private val surfaceBrightDarkHighContrast = Color(0xFF3A3933) private val surfaceContainerLowestDarkHighContrast = Color(0xFF0F0E09) private val surfaceContainerLowDarkHighContrast = Color(0xFF1C1C16) private val surfaceContainerDarkHighContrast = Color(0xFF20201A) private val surfaceContainerHighDarkHighContrast = Color(0xFF2B2A24) private val surfaceContainerHighestDarkHighContrast = Color(0xFF36352F) private val zestfulLightScheme = lightColorScheme( primary = primaryLight, onPrimary = onPrimaryLight, primaryContainer = primaryContainerLight, onPrimaryContainer = onPrimaryContainerLight, secondary = secondaryLight, onSecondary = onSecondaryLight, secondaryContainer = secondaryContainerLight, onSecondaryContainer = onSecondaryContainerLight, tertiary = tertiaryLight, onTertiary = onTertiaryLight, tertiaryContainer = tertiaryContainerLight, onTertiaryContainer = onTertiaryContainerLight, error = errorLight, onError = onErrorLight, errorContainer = errorContainerLight, onErrorContainer = onErrorContainerLight, background = backgroundLight, onBackground = onBackgroundLight, surface = surfaceLight, onSurface = onSurfaceLight, surfaceVariant = surfaceVariantLight, onSurfaceVariant = onSurfaceVariantLight, outline = outlineLight, outlineVariant = outlineVariantLight, scrim = scrimLight, inverseSurface = inverseSurfaceLight, inverseOnSurface = inverseOnSurfaceLight, inversePrimary = inversePrimaryLight, surfaceDim = surfaceDimLight, surfaceBright = surfaceBrightLight, surfaceContainerLowest = surfaceContainerLowestLight, surfaceContainerLow = surfaceContainerLowLight, surfaceContainer = surfaceContainerLight, surfaceContainerHigh = surfaceContainerHighLight, surfaceContainerHighest = surfaceContainerHighestLight, ) private val zestfulDarkScheme = darkColorScheme( primary = primaryDark, onPrimary = onPrimaryDark, primaryContainer = primaryContainerDark, onPrimaryContainer = onPrimaryContainerDark, secondary = secondaryDark, onSecondary = onSecondaryDark, secondaryContainer = secondaryContainerDark, onSecondaryContainer = onSecondaryContainerDark, tertiary = tertiaryDark, onTertiary = onTertiaryDark, tertiaryContainer = tertiaryContainerDark, onTertiaryContainer = onTertiaryContainerDark, error = errorDark, onError = onErrorDark, errorContainer = errorContainerDark, onErrorContainer = onErrorContainerDark, background = backgroundDark, onBackground = onBackgroundDark, surface = surfaceDark, onSurface = onSurfaceDark, surfaceVariant = surfaceVariantDark, onSurfaceVariant = onSurfaceVariantDark, outline = outlineDark, outlineVariant = outlineVariantDark, scrim = scrimDark, inverseSurface = inverseSurfaceDark, inverseOnSurface = inverseOnSurfaceDark, inversePrimary = inversePrimaryDark, surfaceDim = surfaceDimDark, surfaceBright = surfaceBrightDark, surfaceContainerLowest = surfaceContainerLowestDark, surfaceContainerLow = surfaceContainerLowDark, surfaceContainer = surfaceContainerDark, surfaceContainerHigh = surfaceContainerHighDark, surfaceContainerHighest = surfaceContainerHighestDark, ) private val zestfulMediumContrastLightColorScheme = lightColorScheme( primary = primaryLightMediumContrast, onPrimary = onPrimaryLightMediumContrast, primaryContainer = primaryContainerLightMediumContrast, onPrimaryContainer = onPrimaryContainerLightMediumContrast, secondary = secondaryLightMediumContrast, onSecondary = onSecondaryLightMediumContrast, secondaryContainer = secondaryContainerLightMediumContrast, onSecondaryContainer = onSecondaryContainerLightMediumContrast, tertiary = tertiaryLightMediumContrast, onTertiary = onTertiaryLightMediumContrast, tertiaryContainer = tertiaryContainerLightMediumContrast, onTertiaryContainer = onTertiaryContainerLightMediumContrast, error = errorLightMediumContrast, onError = onErrorLightMediumContrast, errorContainer = errorContainerLightMediumContrast, onErrorContainer = onErrorContainerLightMediumContrast, background = backgroundLightMediumContrast, onBackground = onBackgroundLightMediumContrast, surface = surfaceLightMediumContrast, onSurface = onSurfaceLightMediumContrast, surfaceVariant = surfaceVariantLightMediumContrast, onSurfaceVariant = onSurfaceVariantLightMediumContrast, outline = outlineLightMediumContrast, outlineVariant = outlineVariantLightMediumContrast, scrim = scrimLightMediumContrast, inverseSurface = inverseSurfaceLightMediumContrast, inverseOnSurface = inverseOnSurfaceLightMediumContrast, inversePrimary = inversePrimaryLightMediumContrast, surfaceDim = surfaceDimLightMediumContrast, surfaceBright = surfaceBrightLightMediumContrast, surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, surfaceContainerLow = surfaceContainerLowLightMediumContrast, surfaceContainer = surfaceContainerLightMediumContrast, surfaceContainerHigh = surfaceContainerHighLightMediumContrast, surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, ) private val zestfulHighContrastLightColorScheme = lightColorScheme( primary = primaryLightHighContrast, onPrimary = onPrimaryLightHighContrast, primaryContainer = primaryContainerLightHighContrast, onPrimaryContainer = onPrimaryContainerLightHighContrast, secondary = secondaryLightHighContrast, onSecondary = onSecondaryLightHighContrast, secondaryContainer = secondaryContainerLightHighContrast, onSecondaryContainer = onSecondaryContainerLightHighContrast, tertiary = tertiaryLightHighContrast, onTertiary = onTertiaryLightHighContrast, tertiaryContainer = tertiaryContainerLightHighContrast, onTertiaryContainer = onTertiaryContainerLightHighContrast, error = errorLightHighContrast, onError = onErrorLightHighContrast, errorContainer = errorContainerLightHighContrast, onErrorContainer = onErrorContainerLightHighContrast, background = backgroundLightHighContrast, onBackground = onBackgroundLightHighContrast, surface = surfaceLightHighContrast, onSurface = onSurfaceLightHighContrast, surfaceVariant = surfaceVariantLightHighContrast, onSurfaceVariant = onSurfaceVariantLightHighContrast, outline = outlineLightHighContrast, outlineVariant = outlineVariantLightHighContrast, scrim = scrimLightHighContrast, inverseSurface = inverseSurfaceLightHighContrast, inverseOnSurface = inverseOnSurfaceLightHighContrast, inversePrimary = inversePrimaryLightHighContrast, surfaceDim = surfaceDimLightHighContrast, surfaceBright = surfaceBrightLightHighContrast, surfaceContainerLowest = surfaceContainerLowestLightHighContrast, surfaceContainerLow = surfaceContainerLowLightHighContrast, surfaceContainer = surfaceContainerLightHighContrast, surfaceContainerHigh = surfaceContainerHighLightHighContrast, surfaceContainerHighest = surfaceContainerHighestLightHighContrast, ) private val zestfulMediumContrastDarkColorScheme = darkColorScheme( primary = primaryDarkMediumContrast, onPrimary = onPrimaryDarkMediumContrast, primaryContainer = primaryContainerDarkMediumContrast, onPrimaryContainer = onPrimaryContainerDarkMediumContrast, secondary = secondaryDarkMediumContrast, onSecondary = onSecondaryDarkMediumContrast, secondaryContainer = secondaryContainerDarkMediumContrast, onSecondaryContainer = onSecondaryContainerDarkMediumContrast, tertiary = tertiaryDarkMediumContrast, onTertiary = onTertiaryDarkMediumContrast, tertiaryContainer = tertiaryContainerDarkMediumContrast, onTertiaryContainer = onTertiaryContainerDarkMediumContrast, error = errorDarkMediumContrast, onError = onErrorDarkMediumContrast, errorContainer = errorContainerDarkMediumContrast, onErrorContainer = onErrorContainerDarkMediumContrast, background = backgroundDarkMediumContrast, onBackground = onBackgroundDarkMediumContrast, surface = surfaceDarkMediumContrast, onSurface = onSurfaceDarkMediumContrast, surfaceVariant = surfaceVariantDarkMediumContrast, onSurfaceVariant = onSurfaceVariantDarkMediumContrast, outline = outlineDarkMediumContrast, outlineVariant = outlineVariantDarkMediumContrast, scrim = scrimDarkMediumContrast, inverseSurface = inverseSurfaceDarkMediumContrast, inverseOnSurface = inverseOnSurfaceDarkMediumContrast, inversePrimary = inversePrimaryDarkMediumContrast, surfaceDim = surfaceDimDarkMediumContrast, surfaceBright = surfaceBrightDarkMediumContrast, surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, surfaceContainerLow = surfaceContainerLowDarkMediumContrast, surfaceContainer = surfaceContainerDarkMediumContrast, surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, ) private val zestfulHighContrastDarkColorScheme = darkColorScheme( primary = primaryDarkHighContrast, onPrimary = onPrimaryDarkHighContrast, primaryContainer = primaryContainerDarkHighContrast, onPrimaryContainer = onPrimaryContainerDarkHighContrast, secondary = secondaryDarkHighContrast, onSecondary = onSecondaryDarkHighContrast, secondaryContainer = secondaryContainerDarkHighContrast, onSecondaryContainer = onSecondaryContainerDarkHighContrast, tertiary = tertiaryDarkHighContrast, onTertiary = onTertiaryDarkHighContrast, tertiaryContainer = tertiaryContainerDarkHighContrast, onTertiaryContainer = onTertiaryContainerDarkHighContrast, error = errorDarkHighContrast, onError = onErrorDarkHighContrast, errorContainer = errorContainerDarkHighContrast, onErrorContainer = onErrorContainerDarkHighContrast, background = backgroundDarkHighContrast, onBackground = onBackgroundDarkHighContrast, surface = surfaceDarkHighContrast, onSurface = onSurfaceDarkHighContrast, surfaceVariant = surfaceVariantDarkHighContrast, onSurfaceVariant = onSurfaceVariantDarkHighContrast, outline = outlineDarkHighContrast, outlineVariant = outlineVariantDarkHighContrast, scrim = scrimDarkHighContrast, inverseSurface = inverseSurfaceDarkHighContrast, inverseOnSurface = inverseOnSurfaceDarkHighContrast, inversePrimary = inversePrimaryDarkHighContrast, surfaceDim = surfaceDimDarkHighContrast, surfaceBright = surfaceBrightDarkHighContrast, surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, surfaceContainerLow = surfaceContainerLowDarkHighContrast, surfaceContainer = surfaceContainerDarkHighContrast, surfaceContainerHigh = surfaceContainerHighDarkHighContrast, surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, ) val zestfulColorSchemes = ColorSchemes( light = zestfulLightScheme, dark = zestfulDarkScheme, mediumContrastLight = zestfulMediumContrastLightColorScheme, mediumContrastDark = zestfulMediumContrastDarkColorScheme, highContrastLight = zestfulHighContrastLightColorScheme, highContrastDark = zestfulHighContrastDarkColorScheme, nameRes = R.string.label_color_scheme_zestful, ) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/utils/DateFormat.kt ================================================ package com.looker.kenko.utils import java.text.SimpleDateFormat import java.util.* import kotlin.time.Duration.Companion.days import kotlinx.datetime.LocalDate @JvmInline value class DateFormat(private val value: String) { fun format( localDate: LocalDate, locale: Locale = Locale.getDefault(Locale.Category.FORMAT), ): String { val date = Date(localDate.toEpochDays().days.inWholeMilliseconds) val javaFormat = SimpleDateFormat(value, locale) return javaFormat.format(date) } companion object { val SessionLabel = DateFormat("dd-MMM") val BackupName = DateFormat("dd_MM") } } fun formatDate( date: LocalDate, dateTimeFormat: DateFormat = DateFormat.SessionLabel, locale: Locale = Locale.getDefault(Locale.Category.FORMAT), ): String = dateTimeFormat.format(localDate = date, locale = locale) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/utils/DateTime.kt ================================================ /* * 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 . */ package com.looker.kenko.utils import kotlin.random.Random import kotlin.time.Clock import kotlin.time.Instant import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.daysUntil import kotlinx.datetime.format import kotlinx.datetime.format.char import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.todayIn @JvmInline value class EpochDays(val value: Int) operator fun EpochDays.plus(other: EpochDays) = EpochDays(value + other.value) fun LocalDate.toLocalEpochDays() = EpochDays(toEpochDays().toInt()) inline operator fun DayOfWeek.plus(days: Int): DayOfWeek { val amount = (days % 7) return DayOfWeek(((ordinal + (amount + 7)) % 7) + 1) } inline operator fun DayOfWeek.minus(days: Int): DayOfWeek = plus(-days) val LocalDate.isToday: Boolean get() = daysUntil(Clock.System.todayIn(TimeZone.currentSystemDefault())) == 0 fun Instant.toFormat(): String { val dateTime = toLocalDateTime(TimeZone.currentSystemDefault()) val formatter = LocalDateTime.Format { year() char('-') monthNumber() char('-') day() char(' ') hour() char(':') minute() } return dateTime.format(formatter) } fun Random.nextInstant( from: Instant = Instant.DISTANT_PAST, until: Instant = Instant.DISTANT_FUTURE, ): Instant = Instant.fromEpochMilliseconds( nextLong( from = from.toEpochMilliseconds(), until = until.toEpochMilliseconds(), ), ) fun Random.nextLocalDateTime( from: Instant = Instant.DISTANT_PAST, until: Instant = Instant.DISTANT_FUTURE, ): LocalDateTime = nextInstant(from, until) .toLocalDateTime(TimeZone.currentSystemDefault()) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/utils/Path.kt ================================================ /* * 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 . */ package com.looker.kenko.utils import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Path fun Path.moveTo(point: Offset) = moveTo(point.x, point.y) fun Path.lineTo(point: Offset) = lineTo(point.x, point.y) ================================================ FILE: app/src/main/kotlin/com/looker/kenko/utils/Url.kt ================================================ /* * 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 . */ @file:Suppress("NOTHING_TO_INLINE") package com.looker.kenko.utils inline fun String.isValidUrl(): Boolean = startsWith("http://") || startsWith("https://") ================================================ FILE: app/src/main/kotlin/com/looker/kenko/utils/ViewModel.kt ================================================ /* * 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 . */ package com.looker.kenko.utils import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn private const val SharingStartedDefault = 5_000L context(viewModel: ViewModel) fun Flow.asStateFlow( initial: T, coroutineScope: CoroutineScope = viewModel.viewModelScope, started: SharingStarted = SharingStarted.WhileSubscribed(SharingStartedDefault) ): StateFlow = stateIn( scope = coroutineScope, started = started, initialValue = initial ) ================================================ FILE: app/src/main/res/drawable/ic_add.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_app_icon.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_back.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_forward.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_outward.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_check.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_close.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_delete.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_edit.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_history.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_home.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_info.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_keyboard_arrow_left.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_keyboard_arrow_right.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_monochrome.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_lightbulb.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_person.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_radio_button_unchecked.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_remove.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_save.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_settings.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_show_chart.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_tactic.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_verified.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/ic_launcher_background.xml ================================================ #000000 ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Kenko Kenko ケンコウ ○ healthy ○ strong Get Started Continue Start Explore\nSessions Explore\nExercises Add\nExercise Session\nHistory Current\nPlan Sessions Nothing today Did you miss the day? Plans Edit Plan No exercises yet Clean up Clear empty plans? Select a plan name Select exercises Plan name should be easy to remember and not too long. Ideally unique with no special symbols.\n e.g: Push Pull Leg, Strength Cycle, etc. hey there Select Plan Select Plan Current Plan Lifts
    %1$s exercises
\n
    %2$s days
\n
    %3$s rest days
Browse Exercises All Create Add Exercise Edit Target Isometric Reference Exercise requires static holds optional* Exercise Already Exists Add Exercise Search Exercise No exercises planned today Add Set for +%s -%s Settings Name Add Save Yes No Session Exercise .reps .duration .weight Coming Soon Home Performance You Invalid Reference Invalid reference format Invalid URL Unknown Error Enter a plan name Plan name already in use Enter an exercise name Plan is empty Discard Plan?. Press back again to go back. Can\'t Find Session Can\'t Find Exercise Theme System Light Dark Color Palettes Default Dynamic Serene Zestful Twilight Backup & Restore Backup Location Not set Backup Frequency Off Daily Weekly Monthly Backup Now Restore Last Backup: %s Never Backing up… Restoring… Backup completed Restore completed. Please restart the app. Backup failed Restore failed This will replace all current data. Continue? Select backup folder Select backup file Biceps Triceps Back Lats Traps Chest Core Glutes Calves Hamstrings Quads Shoulders .today Plan Select A Plan Start by Selecting a Plan Start your first session Start today\'s session Continue today\'s session Next Your next transformation starts here. No sessions performed Progress Tracking Progressive Overload Statistics Plan Management Fast Feedback you can do it health is in your hands health is wealth workout is my therapy too workout is my therapy too yeah buddy… light weight light weight ain\'t nothing to it but to do it it\'s not about being light i mean it\'s terrific, right\? .mon .tue .wed .thu .fri .sat .sun Monday Tuesday Wednesday Thursday Friday Saturday Sunday
================================================ FILE: app/src/main/res/values/themes.xml ================================================