From 7d381f81e64d47cdff1492d890bbeea64b0591d9 Mon Sep 17 00:00:00 2001 From: pabloscloud <93644977+pabloscloud@users.noreply.github.com> Date: Mon, 15 Apr 2024 18:59:24 +0200 Subject: [PATCH] v.0.17.0 - adds category support - fixes an issue where the app wouldn't switch to the correct year on tablets - updates links and resources - bump libs - bump version --- README.md | 57 ++- app/build.gradle.kts | 8 +- app/release/output-metadata.json | 4 +- .../1.json | 58 +++ .../2.json | 121 +++++ app/src/main/AndroidManifest.xml | 1 - .../java/cloud/pablos/overload/data/Backup.kt | 68 +++ .../cloud/pablos/overload/data/Converters.kt | 43 ++ .../cloud/pablos/overload/data/Helpers.kt | 104 +++++ .../pablos/overload/data/OverloadDatabase.kt | 38 ++ .../pablos/overload/data/category/Category.kt | 27 ++ .../overload/data/category/CategoryDao.kt | 30 ++ .../overload/data/category/CategoryEvent.kt | 29 ++ .../overload/data/category/CategoryState.kt | 21 + .../data/category/CategoryViewModel.kt | 160 +++++++ .../cloud/pablos/overload/data/item/Item.kt | 3 + .../pablos/overload/data/item/ItemDao.kt | 147 ++++--- .../pablos/overload/data/item/ItemDatabase.kt | 9 - .../pablos/overload/data/item/ItemEvent.kt | 2 + .../pablos/overload/data/item/ItemState.kt | 3 + .../overload/data/item/ItemViewModel.kt | 12 +- .../cloud/pablos/overload/ui/MainActivity.kt | 52 ++- .../cloud/pablos/overload/ui/OverloadApp.kt | 148 +++++-- .../java/cloud/pablos/overload/ui/TabItem.kt | 3 +- .../navigation/OverloadNavigationActions.kt | 1 + .../OverloadNavigationComponents.kt | 129 ++++-- .../ui/navigation/OverloadNavigationFab.kt | 81 +++- .../navigation/OverloadNavigationFabSmall.kt | 60 ++- .../ui/screens/category/CategoryScreen.kt | 350 +++++++++++++++ .../category/CategoryScreenBottomAppBar.kt | 76 ++++ .../CategoryScreenDeleteCategoryDialog.kt | 96 ++++ .../category/CategoryScreenGoalDialog.kt} | 53 ++- .../category/CategoryScreenTopAppBar.kt | 30 ++ .../overload/ui/screens/day/DayScreen.kt | 36 +- .../ui/screens/day/DayScreenBottomAppBar.kt | 6 +- .../ui/screens/day/DayScreenTopAppBar.kt | 4 +- .../overload/ui/tabs/calendar/CalendarTab.kt | 153 ++++--- .../ui/tabs/calendar/CalendarTabTopAppBar.kt | 47 +- .../ui/tabs/calendar/CalendarTabYearDialog.kt | 38 +- .../tabs/configurations/ConfigurationsTab.kt | 415 ++++++++++-------- .../ConfigurationsTabCreateCategoryDialog.kt | 383 ++++++++++++++++ .../ConfigurationsTabImportDialog.kt | 98 ----- .../configurations/ConfigurationsTabItem.kt | 115 +++-- .../pablos/overload/ui/tabs/home/HomeTab.kt | 61 ++- .../ui/tabs/home/HomeTabDeleteBottomAppBar.kt | 48 +- .../ui/tabs/home/HomeTabDeletePauseDialog.kt | 17 +- .../ui/tabs/home/HomeTabEditItemDialog.kt | 35 +- .../overload/ui/tabs/home/HomeTabFab.kt | 72 ++- .../overload/ui/tabs/home/HomeTabItems.kt | 21 +- .../ui/tabs/home/HomeTabManualDialog.kt | 73 ++- .../overload/ui/tabs/home/HomeTabProgress.kt | 53 ++- .../overload/ui/tabs/home/HomeTabTopAppBar.kt | 14 +- .../overload/ui/views/AdjustEndDialog.kt | 54 ++- .../overload/ui/views/ChangeCategoryButton.kt | 53 +++ .../overload/ui/views/ChangeYearButton.kt | 54 +++ .../overload/ui/views/DayScreenDayView.kt | 78 ++-- .../cloud/pablos/overload/ui/views/DayView.kt | 92 ++-- .../ui/views/DayViewItemNotOngoing.kt | 20 +- .../overload/ui/views/DayViewItemOngoing.kt | 20 +- .../overload/ui/views/DayViewProgress.kt | 14 +- .../overload/ui/views/DeleteTopAppBar.kt | 10 +- .../overload/ui/views/ForgotToStopDialog.kt | 46 +- .../ui/views/SpreadAcrossDaysDialog.kt | 54 ++- .../overload/ui/views/SwitchCategoryDialog.kt | 112 +++++ .../pablos/overload/ui/views/TextView.kt | 2 +- .../pablos/overload/ui/views/YearView.kt | 110 ++--- .../res/drawable/ic_launcher_background.xml | 74 ---- .../ic_launcher.xml | 0 .../ic_launcher_round.xml | 0 app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 9 +- fastlane/Appfile | 2 - fastlane/Fastfile | 38 -- fastlane/README.md | 48 -- .../metadata/android/en-US/changelogs/100.txt | 10 - .../metadata/android/en-US/changelogs/101.txt | 5 - .../metadata/android/en-US/changelogs/110.txt | 4 - .../metadata/android/en-US/changelogs/111.txt | 4 - .../metadata/android/en-US/changelogs/120.txt | 7 - .../metadata/android/en-US/changelogs/121.txt | 7 - .../metadata/android/en-US/changelogs/122.txt | 2 - .../metadata/android/en-US/changelogs/123.txt | 6 - .../metadata/android/en-US/changelogs/124.txt | 14 - .../metadata/android/en-US/changelogs/130.txt | 1 - .../metadata/android/en-US/changelogs/131.txt | 1 - .../metadata/android/en-US/changelogs/140.txt | 4 - .../metadata/android/en-US/changelogs/141.txt | 1 - .../metadata/android/en-US/changelogs/150.txt | 1 - .../metadata/android/en-US/changelogs/160.txt | 6 - .../metadata/android/en-US/changelogs/93.txt | 1 - .../android/en-US/full_description.txt | 11 - .../metadata/android/en-US/images/icon.png | Bin 16749 -> 0 bytes .../en-US/images/phoneScreenshots/1.png | Bin 176022 -> 0 bytes .../en-US/images/phoneScreenshots/2.png | Bin 219024 -> 0 bytes .../en-US/images/phoneScreenshots/3.png | Bin 204039 -> 0 bytes .../en-US/images/phoneScreenshots/4.png | Bin 216169 -> 0 bytes .../android/en-US/short_description.txt | 1 - fastlane/metadata/android/en-US/title.txt | 1 - fastlane/report.xml | 18 - gradle/libs.versions.toml | 2 +- spotless/copyright.kt | 3 +- 101 files changed, 3361 insertions(+), 1353 deletions(-) create mode 100644 app/schemas/cloud.pablos.overload.data.OverloadDatabase/1.json create mode 100644 app/schemas/cloud.pablos.overload.data.OverloadDatabase/2.json create mode 100644 app/src/main/java/cloud/pablos/overload/data/Backup.kt create mode 100644 app/src/main/java/cloud/pablos/overload/data/Converters.kt create mode 100644 app/src/main/java/cloud/pablos/overload/data/Helpers.kt create mode 100644 app/src/main/java/cloud/pablos/overload/data/OverloadDatabase.kt create mode 100644 app/src/main/java/cloud/pablos/overload/data/category/Category.kt create mode 100644 app/src/main/java/cloud/pablos/overload/data/category/CategoryDao.kt create mode 100644 app/src/main/java/cloud/pablos/overload/data/category/CategoryEvent.kt create mode 100644 app/src/main/java/cloud/pablos/overload/data/category/CategoryState.kt create mode 100644 app/src/main/java/cloud/pablos/overload/data/category/CategoryViewModel.kt delete mode 100644 app/src/main/java/cloud/pablos/overload/data/item/ItemDatabase.kt create mode 100644 app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreen.kt create mode 100644 app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenBottomAppBar.kt create mode 100644 app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenDeleteCategoryDialog.kt rename app/src/main/java/cloud/pablos/overload/ui/{tabs/configurations/ConfigurationsTabGoalDialog.kt => screens/category/CategoryScreenGoalDialog.kt} (78%) create mode 100644 app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenTopAppBar.kt create mode 100644 app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabCreateCategoryDialog.kt delete mode 100644 app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabImportDialog.kt create mode 100644 app/src/main/java/cloud/pablos/overload/ui/views/ChangeCategoryButton.kt create mode 100644 app/src/main/java/cloud/pablos/overload/ui/views/ChangeYearButton.kt create mode 100644 app/src/main/java/cloud/pablos/overload/ui/views/SwitchCategoryDialog.kt delete mode 100644 app/src/main/res/drawable/ic_launcher_background.xml rename app/src/main/res/{mipmap-anydpi-v26 => mipmap-anydpi}/ic_launcher.xml (100%) rename app/src/main/res/{mipmap-anydpi-v26 => mipmap-anydpi}/ic_launcher_round.xml (100%) delete mode 100644 fastlane/Appfile delete mode 100644 fastlane/Fastfile delete mode 100644 fastlane/README.md delete mode 100644 fastlane/metadata/android/en-US/changelogs/100.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/101.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/110.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/111.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/120.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/121.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/122.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/123.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/124.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/130.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/131.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/140.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/141.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/150.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/160.txt delete mode 100644 fastlane/metadata/android/en-US/changelogs/93.txt delete mode 100644 fastlane/metadata/android/en-US/full_description.txt delete mode 100644 fastlane/metadata/android/en-US/images/icon.png delete mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/1.png delete mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/2.png delete mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/3.png delete mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/4.png delete mode 100644 fastlane/metadata/android/en-US/short_description.txt delete mode 100644 fastlane/metadata/android/en-US/title.txt delete mode 100644 fastlane/report.xml diff --git a/README.md b/README.md index 0dff4a35..42b73269 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Translation status](https://translate.codeberg.org/widget/overload/android/svg-badge.svg)](https://translate.codeberg.org/engage/overload/) +[![Translation status](https://badges.crowdin.net/overload/localized.svg)](https://crowdin.com/project/overload) # Overload @@ -9,18 +9,18 @@ Overload is an Android (8.0+) application which allows you to capture timespans Contributions are always welcome! -Just create issues and pull requests in the dev-branch or help [translating](https://translate.codeberg.org/engage/overload/) on Weblate :) +Just create issues and pull requests in the dev-branch or help [translating](https://crowdin.com/project/overload) on Crowdin :) ## Features +- create and manage categories with colors, emojis and goals - create time spans - automatically creates pauses in between - delete time spans - on-by-one or all-together - scroll through days with ease - backup your data - import backups -- set goals ## Feedback @@ -41,59 +41,58 @@ Feedback and suggestions are more than welcome! Please reach out by creating an ## Authors -- [@pabloscloud](https://codeberg.org/pabloscloud) +- [@pabloscloud](https://github.com/pabloscloud) -### Translators +### Kudos + +#### Translators - [@mondstern](https://codeberg.org/mondstern) - [@Vistaus](https://codeberg.org/Vistaus) - [@0que](https://codeberg.org/0que) +#### Projects + +This project is forked from the jetpack compose sample [Reply](https://github.com/android/compose-samples/tree/main/Reply), which is licensed under the Apache License, Version 2.0. + +I used portions of the code of the lovely material you style RSS reader [ReadYou](https://github.com/Ashinch/ReadYou) for the color picker which is licensed under GNU GPLv3. Thanks for the [allowed use](https://github.com/Ashinch/ReadYou/discussions/687) [(archive.org)](https://web.archive.org/web/20240414115828/https://github.com/Ashinch/ReadYou/discussions/687). + ## Screenshots - - - - + + + + - - - - + + + + - - - - + + + +
Screenshot 1Screenshot 2Screenshot 3Screenshot 4Screenshot 1Screenshot 2Screenshot 3Screenshot 4
Screenshot 1Screenshot 2Screenshot 3Screenshot 4Screenshot 1Screenshot 2Screenshot 3Screenshot 4
Screenshot 1Screenshot 2Screenshot 3Screenshot 4Screenshot 1Screenshot 2Screenshot 3Screenshot 4
## FAQ -### How do I import a backup? {#import-backup} +### How do I import a .csv backup? Open your files app, choose the backup file, and locate the share icon or text. Tapping this icon will bring up a menu displaying various apps for sharing. From the list, select Overload to initiate the import. Wait until the import is finished, indicated by a completion message. -### Why does Overload rely on the Systems Sharesheet to import a backup? -Overload utilises the Systems Sharesheet for importing backups instead of requesting broad access to all files on your device. This approach avoids the need to seek permissions that could undermine trust in the project. Moreover, Overload's reliance on the Sharesheet ensures that the app only gains access to the specific file it requires, eliminating the necessity for extensive permissions. - - ### What are ongoing pauses? By showing the duration between the last item and the current time you can determine how long you stopped or paused working since then. You can plan how much longer your pause is at any given moment as the duration updates in real time. -### Why can't I delete an ongoing pause? {#delete-pause} +### Why can't I delete an ongoing pause? You cannot delete ongoing pauses as they are only there to indicate a pause will be created once you hit start again. If you will not hit start until the next day it will be gone and will not count against your goal. By deleting items that occurred beforehand, you can hide the pause. Imagine you're using the app to track your work hours. You take a break, and an ongoing pause is created. If you get ill during the day or decide not to work on the day for any other reason, the ongoing pause will vanish without affecting your work-time goal. -### Why does the app annoy me with a popup to adjust the end? {#spread-across-days} +### Why does the app annoy me with a popup to adjust the end? Sometimes we forget stuff. Sometimes we do stuff across days - like sleeping. Nevertheless when an item is still ongoing from yesterday or days before yesterday, you need to set an end or confirm you want to spread the item across multiple days. The popup will be gone once there are no more ongoing items from the past days ;) - - -## Related - -This project draws inspiration from the jetpack compose sample [Reply](https://github.com/android/compose-samples/tree/main/Reply), which is licensed under the Apache License, Version 2.0. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5551141b..6c028a31 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,15 +12,15 @@ android { applicationId = "cloud.pablos.overload" minSdk = 26 targetSdk = 34 - versionCode = 160 - versionName = "0.16.0" + versionCode = 170 + versionName = "0.17.0" vectorDrawables.useSupportLibrary = true testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" kapt { arguments { - arg("room.schemaLocation", "") - arg("room.exportSchema", "false") + arg("room.schemaLocation", "$projectDir/schemas") + arg("room.exportSchema", "true") } } } diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 1c491238..05a22bfb 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 160, - "versionName": "0.16.0", + "versionCode": 170, + "versionName": "0.17.0", "outputFile": "app-release.apk" } ], diff --git a/app/schemas/cloud.pablos.overload.data.OverloadDatabase/1.json b/app/schemas/cloud.pablos.overload.data.OverloadDatabase/1.json new file mode 100644 index 00000000..59066736 --- /dev/null +++ b/app/schemas/cloud.pablos.overload.data.OverloadDatabase/1.json @@ -0,0 +1,58 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "dd6c87d009809e7cd6f2f504a6eba8d2", + "entities": [ + { + "tableName": "items", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `startTime` TEXT NOT NULL, `endTime` TEXT NOT NULL, `ongoing` INTEGER NOT NULL, `pause` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ongoing", + "columnName": "ongoing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pause", + "columnName": "pause", + "affinity": "INTEGER", + "notNull": true + } + ], + "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, 'dd6c87d009809e7cd6f2f504a6eba8d2')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/cloud.pablos.overload.data.OverloadDatabase/2.json b/app/schemas/cloud.pablos.overload.data.OverloadDatabase/2.json new file mode 100644 index 00000000..4f3e3fc5 --- /dev/null +++ b/app/schemas/cloud.pablos.overload.data.OverloadDatabase/2.json @@ -0,0 +1,121 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "484ddb3056be6218a18539a4d1875f90", + "entities": [ + { + "tableName": "categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `color` INTEGER NOT NULL, `emoji` TEXT NOT NULL, `goal1` INTEGER NOT NULL, `goal2` INTEGER NOT NULL, `isDefault` INTEGER NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emoji", + "columnName": "emoji", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "goal1", + "columnName": "goal1", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "goal2", + "columnName": "goal2", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDefault", + "columnName": "isDefault", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "items", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `startTime` TEXT NOT NULL, `endTime` TEXT NOT NULL, `ongoing` INTEGER NOT NULL, `pause` INTEGER NOT NULL, `categoryId` INTEGER NOT NULL DEFAULT 1)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ongoing", + "columnName": "ongoing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pause", + "columnName": "pause", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + } + ], + "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, '484ddb3056be6218a18539a4d1875f90')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d870f513..8930c661 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,5 +46,4 @@ android:resource="@xml/file_paths" /> - diff --git a/app/src/main/java/cloud/pablos/overload/data/Backup.kt b/app/src/main/java/cloud/pablos/overload/data/Backup.kt new file mode 100644 index 00000000..e3779240 --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/data/Backup.kt @@ -0,0 +1,68 @@ +package cloud.pablos.overload.data + +import cloud.pablos.overload.data.category.CategoryState +import cloud.pablos.overload.data.item.ItemState +import com.google.gson.Gson +import java.time.LocalDateTime + +class Backup { + data class DatabaseBackup( + val data: Map>>, + val backupVersion: Int, + val backupDate: String, + ) + + companion object { + fun backupToJson( + categoryState: CategoryState, + itemState: ItemState, + ): String { + val gson = Gson() + + val categoriesTable = + categoryState.categories.map { category -> + mapOf( + "id" to category.id, + "color" to category.color, + "emoji" to category.emoji, + "goal1" to category.goal1, + "goal2" to category.goal2, + "isDefault" to category.isDefault, + "name" to category.name, + ) + } + + val itemsTable = + itemState.items.map { item -> + mapOf( + "id" to item.id, + "startTime" to item.startTime, + "endTime" to item.endTime, + "ongoing" to item.ongoing, + "pause" to item.pause, + "categoryId" to item.categoryId, + ) + } + + val data = + mapOf( + "categories" to categoriesTable, + "items" to itemsTable, + ) + + val backup = + DatabaseBackup( + data, + 2, + LocalDateTime.now().toString(), + ) + + return try { + val json = gson.toJson(backup) + json + } catch (e: Exception) { + "{}" + } + } + } +} diff --git a/app/src/main/java/cloud/pablos/overload/data/Converters.kt b/app/src/main/java/cloud/pablos/overload/data/Converters.kt new file mode 100644 index 00000000..915ec2ee --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/data/Converters.kt @@ -0,0 +1,43 @@ +package cloud.pablos.overload.data + +import androidx.compose.ui.graphics.Color +import java.time.LocalDateTime +import java.time.format.DateTimeFormatterBuilder +import java.time.format.DateTimeParseException +import java.time.temporal.ChronoField + +class Converters { + companion object { + fun convertColorToLong(color: Color): Long { + val alpha = 255 // (color.alpha * 255).toInt() + val red = (color.red * 255).toInt() + val green = (color.green * 255).toInt() + val blue = (color.blue * 255).toInt() + return (alpha.toLong() shl 24) or (red.toLong() shl 16) or (green.toLong() shl 8) or blue.toLong() + } + + fun convertLongToColor(value: Long): Color { + val alpha = 1f // (value shr 24 and 0xFF).toFloat() / 255f + val red = (value shr 16 and 0xFF).toFloat() / 255f + val green = (value shr 8 and 0xFF).toFloat() / 255f + val blue = (value and 0xFF).toFloat() / 255f + return Color(red, green, blue, alpha) + } + + fun convertStringToLocalDateTime(dateTimeString: String): LocalDateTime { + val formatter = + DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss") + .optionalStart() + .appendFraction(ChronoField.MICRO_OF_SECOND, 0, 6, true) + .optionalEnd() + .toFormatter() + + return try { + LocalDateTime.parse(dateTimeString, formatter) + } catch (e: DateTimeParseException) { + return LocalDateTime.now() + } + } + } +} diff --git a/app/src/main/java/cloud/pablos/overload/data/Helpers.kt b/app/src/main/java/cloud/pablos/overload/data/Helpers.kt new file mode 100644 index 00000000..05dcfef1 --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/data/Helpers.kt @@ -0,0 +1,104 @@ +package cloud.pablos.overload.data + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import cloud.pablos.overload.data.Converters.Companion.convertStringToLocalDateTime +import cloud.pablos.overload.data.category.Category +import cloud.pablos.overload.data.category.CategoryState +import cloud.pablos.overload.data.item.Item +import cloud.pablos.overload.data.item.ItemState +import cloud.pablos.overload.ui.views.extractDate +import cloud.pablos.overload.ui.views.getLocalDate +import java.time.LocalDate +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow + +class Helpers { + companion object { + private fun calculateRelativeLuminance(color: Color): Double { + val red = if (color.red <= 0.03928) color.red / 12.92 else ((color.red + 0.055) / 1.055).pow(2.4) + val green = if (color.green <= 0.03928) color.green / 12.92 else ((color.green + 0.055) / 1.055).pow(2.4) + val blue = if (color.blue <= 0.03928) color.blue / 12.92 else ((color.blue + 0.055) / 1.055).pow(2.4) + return 0.2126 * red + 0.7152 * green + 0.0722 * blue + } + + private fun calculateContrastRatio( + background: Color, + foreground: Color, + ): Double { + val lum1 = calculateRelativeLuminance(background) + val lum2 = calculateRelativeLuminance(foreground) + val lighter = max(lum1, lum2) + val darker = min(lum1, lum2) + return (lighter + 0.05) / (darker + 0.05) + } + + @Composable + fun decideBackground(categoryState: CategoryState): Color { + return ( + categoryState.categories.find { category -> + category.id == categoryState.selectedCategory + } + ) + ?.let { + if (it.isDefault) { + MaterialTheme.colorScheme.primaryContainer + } else { + Converters.convertLongToColor(it.color) + } + } + ?: MaterialTheme.colorScheme.primaryContainer + } + + @Composable + fun decideForeground(background: Color): Color { + val white = MaterialTheme.colorScheme.background + val black = MaterialTheme.colorScheme.onBackground + val contrastWithWhite = calculateContrastRatio(background, white) + val contrastWithBlack = calculateContrastRatio(background, black) + return if (contrastWithWhite >= contrastWithBlack) white else black + } + + fun getSelectedDay(itemState: ItemState): LocalDate { + return itemState.selectedDayCalendar.takeIf { it.isNotBlank() }?.let { getLocalDate(it) } + ?: LocalDate.now() + } + + fun getSelectedCategory(categoryState: CategoryState): Category? { + return categoryState.categories.find { it.id == categoryState.selectedCategory } + } + + fun getItems( + categoryState: CategoryState, + itemState: ItemState, + date: LocalDate? = null, + ): List { + return itemState.items.filter { item -> + val startTime = convertStringToLocalDateTime(item.startTime) + val categoryId = categoryState.selectedCategory + + if (date != null) { + categoryId == item.categoryId && + extractDate(startTime) == date + } else { + categoryId == item.categoryId + } + } + } + + fun getItemsPastDays( + categoryState: CategoryState, + itemState: ItemState, + ): List { + return itemState.items.filter { item -> + val startTime = convertStringToLocalDateTime(item.startTime) + val categoryId = categoryState.selectedCategory + + categoryId == item.categoryId && + extractDate(startTime) != LocalDate.now() + } + } + } +} diff --git a/app/src/main/java/cloud/pablos/overload/data/OverloadDatabase.kt b/app/src/main/java/cloud/pablos/overload/data/OverloadDatabase.kt new file mode 100644 index 00000000..7e173e57 --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/data/OverloadDatabase.kt @@ -0,0 +1,38 @@ +package cloud.pablos.overload.data + +import android.content.Context +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import cloud.pablos.overload.data.category.Category +import cloud.pablos.overload.data.category.CategoryDao +import cloud.pablos.overload.data.item.Item +import cloud.pablos.overload.data.item.ItemDao + +@Database( + entities = [Category::class, Item::class], + version = 2, + exportSchema = true, + autoMigrations = [ + AutoMigration(from = 1, to = 2), + ], +) +abstract class OverloadDatabase : RoomDatabase() { + abstract fun categoryDao(): CategoryDao + + abstract fun itemDao(): ItemDao + + companion object { + private const val DATABASE_NAME = "items" + + @Volatile private var instance: OverloadDatabase? = null + + private fun buildDatabase(context: Context) = + Room.databaseBuilder( + context.applicationContext, + OverloadDatabase::class.java, + DATABASE_NAME, + ).build() + } +} diff --git a/app/src/main/java/cloud/pablos/overload/data/category/Category.kt b/app/src/main/java/cloud/pablos/overload/data/category/Category.kt new file mode 100644 index 00000000..bad54e88 --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/data/category/Category.kt @@ -0,0 +1,27 @@ +package cloud.pablos.overload.data.category + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.Relation +import cloud.pablos.overload.data.item.Item + +@Entity(tableName = "categories") +data class Category( + @PrimaryKey(autoGenerate = true) val id: Int = 1, + val color: Long, + val emoji: String, + val goal1: Int, + val goal2: Int, + val isDefault: Boolean, + val name: String, +) + +data class CategoryWithItems( + @Embedded val category: Category, + @Relation( + parentColumn = "id", + entityColumn = "categoryId", + ) + val items: List, +) diff --git a/app/src/main/java/cloud/pablos/overload/data/category/CategoryDao.kt b/app/src/main/java/cloud/pablos/overload/data/category/CategoryDao.kt new file mode 100644 index 00000000..5d28b243 --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/data/category/CategoryDao.kt @@ -0,0 +1,30 @@ +package cloud.pablos.overload.data.category + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Upsert +import kotlinx.coroutines.flow.Flow + +@Dao +interface CategoryDao { + @Upsert + suspend fun upsertCategory(category: Category) + +// @Upsert +// suspend fun upsertCategories(items: List) + + @Delete + suspend fun deleteCategory(category: Category) + +// @Delete +// suspend fun deleteCategories(items: List) + + @Query("SELECT * FROM categories") + fun getAllCategories(): Flow> + + @Transaction + @Query("SELECT * FROM categories") + fun getCategoryWithItems(): Flow> +} diff --git a/app/src/main/java/cloud/pablos/overload/data/category/CategoryEvent.kt b/app/src/main/java/cloud/pablos/overload/data/category/CategoryEvent.kt new file mode 100644 index 00000000..64c45c7c --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/data/category/CategoryEvent.kt @@ -0,0 +1,29 @@ +package cloud.pablos.overload.data.category + +sealed interface CategoryEvent { + data object SaveCategory : CategoryEvent + + data class SetId(val id: Int) : CategoryEvent + + data class SetColor(val color: Long) : CategoryEvent + + data class SetEmoji(val emoji: String) : CategoryEvent + + data class SetGoal1(val goal1: Int) : CategoryEvent + + data class SetGoal2(val goal2: Int) : CategoryEvent + + data class SetName(val name: String) : CategoryEvent + + data class SetIsDefault(val isDefault: Boolean) : CategoryEvent + + data class DeleteCategory(val category: Category) : CategoryEvent + + data class SetSelectedCategoryConfigurations(val selectedCategoryConfigurations: Int) : CategoryEvent + + data class SetSelectedCategory(val selectedCategory: Int) : CategoryEvent + + data class SetIsCreateCategoryDialogOpenHome(val isCreateCategoryDialogOpenHome: Boolean) : CategoryEvent + + data class SetIsSwitchCategoryDialogOpenHome(val isSwitchCategoryDialogOpenHome: Boolean) : CategoryEvent +} diff --git a/app/src/main/java/cloud/pablos/overload/data/category/CategoryState.kt b/app/src/main/java/cloud/pablos/overload/data/category/CategoryState.kt new file mode 100644 index 00000000..004e68f4 --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/data/category/CategoryState.kt @@ -0,0 +1,21 @@ +package cloud.pablos.overload.data.category + +import androidx.compose.ui.graphics.Color + +data class CategoryState( + val categories: List = emptyList(), + val categoryWithItems: List = emptyList(), + // -- + val id: Int = 1, + val color: Long = Color.Unspecified.value.toLong(), + val emoji: String = "🕣", + val goal1: Int = 0, + val goal2: Int = 0, + val name: String = "Default", + val isDefault: Boolean = true, + // -- + val selectedCategoryConfigurations: Int = 1, + val selectedCategory: Int = 1, + val isCreateCategoryDialogOpenHome: Boolean = false, + val isSwitchCategoryDialogOpenHome: Boolean = false, +) diff --git a/app/src/main/java/cloud/pablos/overload/data/category/CategoryViewModel.kt b/app/src/main/java/cloud/pablos/overload/data/category/CategoryViewModel.kt new file mode 100644 index 00000000..3da6b93b --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/data/category/CategoryViewModel.kt @@ -0,0 +1,160 @@ +package cloud.pablos.overload.data.category + +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import cloud.pablos.overload.data.Converters.Companion.convertColorToLong +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class CategoryViewModel( + private val dao: CategoryDao, +) : ViewModel() { + private val _categories = + dao.getAllCategories().stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + private val _state = MutableStateFlow(CategoryState()) + val state = + combine(_state, _categories) { state, categories -> + state.copy( + categories = categories, + ) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), CategoryState()) + + fun categoryEvent(event: CategoryEvent) { + when (event) { + is CategoryEvent.DeleteCategory -> { + viewModelScope.launch { + dao.deleteCategory(event.category) + } + } + + CategoryEvent.SaveCategory -> { + val id = _state.value.id + val color = _state.value.color + val emoji = _state.value.emoji + val goal1 = _state.value.goal1 + val goal2 = _state.value.goal2 + val isDefault = _state.value.isDefault + val name = _state.value.name + + val category = + Category( + id = id, + color = color, + emoji = emoji, + goal1 = goal1, + goal2 = goal2, + isDefault = isDefault, + name = name, + ) + + viewModelScope.launch { + dao.upsertCategory(category) + } + + _state.update { + it.copy( + id = 0, + color = convertColorToLong(Color.Unspecified), + emoji = "🕣", + goal1 = 0, + goal2 = 0, + isDefault = false, + name = "", + ) + } + } + + is CategoryEvent.SetColor -> { + _state.update { + it.copy( + color = event.color, + ) + } + } + + is CategoryEvent.SetEmoji -> { + _state.update { + it.copy( + emoji = event.emoji, + ) + } + } + + is CategoryEvent.SetIsDefault -> { + _state.update { + it.copy( + isDefault = event.isDefault, + ) + } + } + + is CategoryEvent.SetId -> { + _state.update { + it.copy( + id = event.id, + ) + } + } + + is CategoryEvent.SetName -> { + _state.update { + it.copy( + name = event.name, + ) + } + } + + is CategoryEvent.SetSelectedCategoryConfigurations -> { + _state.update { + it.copy( + selectedCategoryConfigurations = event.selectedCategoryConfigurations, + ) + } + } + + is CategoryEvent.SetSelectedCategory -> { + _state.update { + it.copy( + selectedCategory = event.selectedCategory, + ) + } + } + + is CategoryEvent.SetIsCreateCategoryDialogOpenHome -> { + _state.update { + it.copy( + isCreateCategoryDialogOpenHome = event.isCreateCategoryDialogOpenHome, + ) + } + } + + is CategoryEvent.SetGoal1 -> { + _state.update { + it.copy( + goal1 = event.goal1, + ) + } + } + is CategoryEvent.SetGoal2 -> { + _state.update { + it.copy( + goal2 = event.goal2, + ) + } + } + + is CategoryEvent.SetIsSwitchCategoryDialogOpenHome -> { + _state.update { + it.copy( + isSwitchCategoryDialogOpenHome = event.isSwitchCategoryDialogOpenHome, + ) + } + } + } + } +} diff --git a/app/src/main/java/cloud/pablos/overload/data/item/Item.kt b/app/src/main/java/cloud/pablos/overload/data/item/Item.kt index 3667930d..7391bb78 100644 --- a/app/src/main/java/cloud/pablos/overload/data/item/Item.kt +++ b/app/src/main/java/cloud/pablos/overload/data/item/Item.kt @@ -1,5 +1,6 @@ package cloud.pablos.overload.data.item +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @@ -10,4 +11,6 @@ data class Item( val endTime: String, val ongoing: Boolean, val pause: Boolean, + @ColumnInfo(defaultValue = "1") + val categoryId: Int, ) diff --git a/app/src/main/java/cloud/pablos/overload/data/item/ItemDao.kt b/app/src/main/java/cloud/pablos/overload/data/item/ItemDao.kt index 2721d3ab..6f85cf6c 100644 --- a/app/src/main/java/cloud/pablos/overload/data/item/ItemDao.kt +++ b/app/src/main/java/cloud/pablos/overload/data/item/ItemDao.kt @@ -1,12 +1,16 @@ package cloud.pablos.overload.data.item +import androidx.compose.ui.graphics.Color import androidx.room.Dao import androidx.room.Delete import androidx.room.Query import androidx.room.Upsert -import cloud.pablos.overload.ui.tabs.home.getItemsOfDay -import cloud.pablos.overload.ui.views.extractDate -import cloud.pablos.overload.ui.views.parseToLocalDateTime +import cloud.pablos.overload.data.Converters.Companion.convertColorToLong +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.Helpers.Companion.getItemsPastDays +import cloud.pablos.overload.data.Helpers.Companion.getSelectedCategory +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState import kotlinx.coroutines.flow.Flow import java.time.LocalDate import java.time.LocalDateTime @@ -29,73 +33,96 @@ interface ItemDao { fun getAllItems(): Flow> } -// Export function -fun backupItemsToCsv(state: ItemState): String { - val items = state.items - /*val gson = Gson() - - return try { - val json = gson.toJson(items) - json - } catch (e: Exception) { - "{}" // Return a placeholder JSON object - }*/ - - val csvHeader = "id,startTime,endTime,ongoing,pause\n" - val csvData = - items.joinToString("\n") { item -> - "${item.id},${item.startTime},${item.endTime},${item.ongoing},${item.pause}" - } - - return csvHeader + csvData +fun fabPress( + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, +) { + val categories = categoryState.categories + + if (categories.isNotEmpty()) { + startOrStop(categoryState, categoryEvent, itemState, itemEvent) + } else if (itemState.items.isNotEmpty()) { + categoryEvent(CategoryEvent.SetId(1)) + categoryEvent(CategoryEvent.SetName("Default")) + categoryEvent(CategoryEvent.SetColor(convertColorToLong(Color(204, 230, 255)))) + categoryEvent(CategoryEvent.SetGoal1(0)) + categoryEvent(CategoryEvent.SetGoal2(0)) + categoryEvent(CategoryEvent.SetEmoji("🕣")) + categoryEvent(CategoryEvent.SetIsDefault(true)) + categoryEvent(CategoryEvent.SaveCategory) + + startOrStop(categoryState, categoryEvent, itemState, itemEvent) + } else { + categoryEvent(CategoryEvent.SetIsCreateCategoryDialogOpenHome(true)) + } } -fun startOrStopPause( - state: ItemState, - onEvent: (ItemEvent) -> Unit, +fun startOrStop( + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { val date = LocalDate.now() + val selectedCategory = getSelectedCategory(categoryState) + val selectedCategoryId: Int? + + if (selectedCategory != null) { + selectedCategoryId = selectedCategory.id + } else if (categoryState.categories.isEmpty()) { + categoryEvent(CategoryEvent.SetIsCreateCategoryDialogOpenHome(true)) + return + } else { + selectedCategoryId = categoryState.categories.first().id + categoryEvent(CategoryEvent.SetSelectedCategory(selectedCategoryId)) - val itemsForToday = getItemsOfDay(date, state) - val isFirstToday = itemsForToday.isEmpty() - val isOngoingToday = itemsForToday.isNotEmpty() && itemsForToday.last().ongoing - - val itemsNotToday = - state.items.filter { item -> - val startTime = parseToLocalDateTime(item.startTime) - extractDate(startTime) != date + if (categoryState.categories.count() > 1) { + return } - val isOngoingNotToday = itemsNotToday.isNotEmpty() && itemsNotToday.any { it.ongoing } + } + + val itemsToday = getItems(categoryState, itemState, date) + val isFirstToday = itemsToday.isEmpty() + val isOngoingToday = itemsToday.isNotEmpty() && itemsToday.last().ongoing - if (isOngoingNotToday) { - onEvent(ItemEvent.SetForgotToStopDialogShown(true)) + val itemsPastDays = getItemsPastDays(categoryState, itemState) + val itemsOngoingNotToday = itemsPastDays.isNotEmpty() && itemsPastDays.any { it.ongoing } + + if (itemsOngoingNotToday) { + itemEvent(ItemEvent.SetForgotToStopDialogShown(true)) } else if (isFirstToday) { - onEvent(ItemEvent.SetStart(LocalDateTime.now().toString())) - onEvent(ItemEvent.SetOngoing(true)) - onEvent(ItemEvent.SetPause(false)) - onEvent(ItemEvent.SaveItem) + itemEvent(ItemEvent.SetCategoryId(selectedCategoryId)) + itemEvent(ItemEvent.SetStart(LocalDateTime.now().toString())) + itemEvent(ItemEvent.SetOngoing(true)) + itemEvent(ItemEvent.SetPause(false)) + itemEvent(ItemEvent.SaveItem) - onEvent(ItemEvent.SetIsOngoing(true)) + itemEvent(ItemEvent.SetIsOngoing(true)) } else if (isOngoingToday) { - onEvent(ItemEvent.SetId(itemsForToday.last().id)) - onEvent(ItemEvent.SetStart(itemsForToday.last().startTime)) - onEvent(ItemEvent.SetEnd(LocalDateTime.now().toString())) - onEvent(ItemEvent.SetOngoing(false)) - onEvent(ItemEvent.SaveItem) - - onEvent(ItemEvent.SetIsOngoing(false)) + itemEvent(ItemEvent.SetCategoryId(selectedCategoryId)) + itemEvent(ItemEvent.SetId(itemsToday.last().id)) + itemEvent(ItemEvent.SetStart(itemsToday.last().startTime)) + itemEvent(ItemEvent.SetEnd(LocalDateTime.now().toString())) + itemEvent(ItemEvent.SetOngoing(false)) + itemEvent(ItemEvent.SaveItem) + + itemEvent(ItemEvent.SetIsOngoing(false)) } else { - onEvent(ItemEvent.SetStart(itemsForToday.last().endTime)) - onEvent(ItemEvent.SetEnd(LocalDateTime.now().toString())) - onEvent(ItemEvent.SetOngoing(false)) - onEvent(ItemEvent.SetPause(true)) - onEvent(ItemEvent.SaveItem) - - onEvent(ItemEvent.SetStart(LocalDateTime.now().toString())) - onEvent(ItemEvent.SetOngoing(true)) - onEvent(ItemEvent.SetPause(false)) - onEvent(ItemEvent.SaveItem) - - onEvent(ItemEvent.SetIsOngoing(true)) + itemEvent(ItemEvent.SetCategoryId(selectedCategoryId)) + itemEvent(ItemEvent.SetStart(itemsToday.last().endTime)) + itemEvent(ItemEvent.SetEnd(LocalDateTime.now().toString())) + itemEvent(ItemEvent.SetOngoing(false)) + itemEvent(ItemEvent.SetPause(true)) + itemEvent(ItemEvent.SaveItem) + + itemEvent(ItemEvent.SetCategoryId(selectedCategoryId)) + itemEvent(ItemEvent.SetStart(LocalDateTime.now().toString())) + itemEvent(ItemEvent.SetOngoing(true)) + itemEvent(ItemEvent.SetPause(false)) + itemEvent(ItemEvent.SaveItem) + + itemEvent(ItemEvent.SetIsOngoing(true)) } } diff --git a/app/src/main/java/cloud/pablos/overload/data/item/ItemDatabase.kt b/app/src/main/java/cloud/pablos/overload/data/item/ItemDatabase.kt deleted file mode 100644 index ddc4a617..00000000 --- a/app/src/main/java/cloud/pablos/overload/data/item/ItemDatabase.kt +++ /dev/null @@ -1,9 +0,0 @@ -package cloud.pablos.overload.data.item - -import androidx.room.Database -import androidx.room.RoomDatabase - -@Database(entities = [Item::class], version = 1) -abstract class ItemDatabase : RoomDatabase() { - abstract fun itemDao(): ItemDao -} diff --git a/app/src/main/java/cloud/pablos/overload/data/item/ItemEvent.kt b/app/src/main/java/cloud/pablos/overload/data/item/ItemEvent.kt index 0e61eacd..6553314a 100644 --- a/app/src/main/java/cloud/pablos/overload/data/item/ItemEvent.kt +++ b/app/src/main/java/cloud/pablos/overload/data/item/ItemEvent.kt @@ -3,6 +3,8 @@ package cloud.pablos.overload.data.item sealed interface ItemEvent { data object SaveItem : ItemEvent + data class SetCategoryId(val categoryId: Int) : ItemEvent + data class SetId(val id: Int) : ItemEvent data class SetStart(val start: String) : ItemEvent diff --git a/app/src/main/java/cloud/pablos/overload/data/item/ItemState.kt b/app/src/main/java/cloud/pablos/overload/data/item/ItemState.kt index 3f6687ef..a1967277 100644 --- a/app/src/main/java/cloud/pablos/overload/data/item/ItemState.kt +++ b/app/src/main/java/cloud/pablos/overload/data/item/ItemState.kt @@ -4,12 +4,15 @@ import java.time.LocalDateTime data class ItemState( val items: List = emptyList(), + // -- val id: Int = 0, val start: String = LocalDateTime.now().toString(), val end: String = "", val ongoing: Boolean = false, val pause: Boolean = false, val isOngoing: Boolean = false, + val categoryId: Int = 1, + // -- val selectedDayCalendar: String = "", val selectedYearCalendar: Int = 0, val isDeletingHome: Boolean = false, diff --git a/app/src/main/java/cloud/pablos/overload/data/item/ItemViewModel.kt b/app/src/main/java/cloud/pablos/overload/data/item/ItemViewModel.kt index 7770ac38..b4bef5ee 100644 --- a/app/src/main/java/cloud/pablos/overload/data/item/ItemViewModel.kt +++ b/app/src/main/java/cloud/pablos/overload/data/item/ItemViewModel.kt @@ -23,7 +23,7 @@ class ItemViewModel( ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ItemState()) - fun onEvent(event: ItemEvent) { + fun itemEvent(event: ItemEvent) { when (event) { is ItemEvent.DeleteItems -> { viewModelScope.launch { @@ -44,6 +44,7 @@ class ItemViewModel( val end = _state.value.end val ongoing = _state.value.ongoing val pause = _state.value.pause + val categoryId = _state.value.categoryId val item = Item( @@ -52,6 +53,7 @@ class ItemViewModel( endTime = end, ongoing = ongoing, pause = pause, + categoryId = categoryId, ) viewModelScope.launch { @@ -186,6 +188,14 @@ class ItemViewModel( ) } } + + is ItemEvent.SetCategoryId -> { + _state.update { + it.copy( + categoryId = event.categoryId, + ) + } + } } } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/MainActivity.kt b/app/src/main/java/cloud/pablos/overload/ui/MainActivity.kt index 287e6fed..fff6687b 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/MainActivity.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/MainActivity.kt @@ -1,12 +1,14 @@ package cloud.pablos.overload.ui import android.annotation.SuppressLint +import android.app.Activity import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi @@ -19,22 +21,36 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.room.Room import cloud.pablos.overload.R -import cloud.pablos.overload.data.item.ItemDatabase +import cloud.pablos.overload.data.OverloadDatabase +import cloud.pablos.overload.data.category.CategoryViewModel import cloud.pablos.overload.data.item.ItemViewModel import cloud.pablos.overload.ui.tabs.configurations.handleIntent +import cloud.pablos.overload.ui.tabs.configurations.importJsonFile +import cloud.pablos.overload.ui.tabs.configurations.showImportFailedToast import cloud.pablos.overload.ui.theme.OverloadTheme import com.google.accompanist.adaptive.calculateDisplayFeatures +import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { private val db by lazy { Room.databaseBuilder( applicationContext, - ItemDatabase::class.java, + OverloadDatabase::class.java, "items", ).build() } - private val viewModel by viewModels( + private val categoryViewModel by viewModels( + factoryProducer = { + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return CategoryViewModel(db.categoryDao()) as T + } + } + }, + ) + + private val itemViewModel by viewModels( factoryProducer = { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -44,6 +60,22 @@ class MainActivity : ComponentActivity() { }, ) + private val filePickerLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val uri = result.data?.data + uri?.let { + lifecycleScope.launch { + importJsonFile(it, contentResolver, applicationContext, db, lifecycleScope) + } + } ?: run { + showImportFailedToast(applicationContext) + } + } else { + showImportFailedToast(applicationContext) + } + } + @SuppressLint("SourceLockedOrientationActivity") // The text of dialogs does not fit the screen when not in portrait @RequiresApi(Build.VERSION_CODES.S) @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @@ -64,14 +96,20 @@ class MainActivity : ComponentActivity() { val windowSize = calculateWindowSizeClass(this) val displayFeatures = calculateDisplayFeatures(this) - val state by viewModel.state.collectAsState() - val onEvent = viewModel::onEvent + val categoryState by categoryViewModel.state.collectAsState() + val categoryEvent = categoryViewModel::categoryEvent + + val itemState by itemViewModel.state.collectAsState() + val itemEvent = itemViewModel::itemEvent OverloadApp( windowSize = windowSize, displayFeatures = displayFeatures, - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, + filePickerLauncher = filePickerLauncher, ) } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/OverloadApp.kt b/app/src/main/java/cloud/pablos/overload/ui/OverloadApp.kt index 0be16985..617fd32b 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/OverloadApp.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/OverloadApp.kt @@ -1,6 +1,8 @@ package cloud.pablos.overload.ui +import android.content.Intent import android.os.Build +import androidx.activity.result.ActivityResultLauncher import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background @@ -34,6 +36,8 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.window.layout.DisplayFeature import androidx.window.layout.FoldingFeature +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState import cloud.pablos.overload.ui.navigation.ModalNavigationDrawerContent @@ -42,6 +46,7 @@ import cloud.pablos.overload.ui.navigation.OverloadNavigationActions import cloud.pablos.overload.ui.navigation.OverloadNavigationRail import cloud.pablos.overload.ui.navigation.OverloadRoute import cloud.pablos.overload.ui.navigation.OverloadTopLevelDestination +import cloud.pablos.overload.ui.screens.category.CategoryScreen import cloud.pablos.overload.ui.screens.day.DayScreen import cloud.pablos.overload.ui.tabs.calendar.CalendarTab import cloud.pablos.overload.ui.tabs.configurations.ConfigurationsTab @@ -62,8 +67,11 @@ import kotlinx.coroutines.launch fun OverloadApp( windowSize: WindowSizeClass, displayFeatures: List, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, + filePickerLauncher: ActivityResultLauncher, ) { val navigationType: OverloadNavigationType val contentType: OverloadContentType @@ -134,8 +142,11 @@ fun OverloadApp( navigationType = navigationType, contentType = contentType, navigationContentPosition = navigationContentPosition, - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, + filePickerLauncher = filePickerLauncher, ) } @@ -145,8 +156,11 @@ private fun OverloadNavigationWrapper( navigationType: OverloadNavigationType, contentType: OverloadContentType, navigationContentPosition: OverloadNavigationContentPosition, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, + filePickerLauncher: ActivityResultLauncher, ) { val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() @@ -174,8 +188,11 @@ private fun OverloadNavigationWrapper( drawerState.open() } }, - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, + filePickerLauncher = filePickerLauncher, ) } @@ -191,8 +208,10 @@ private fun OverloadNavigationWrapper( drawerState.close() } }, - state = state, - onEvent = onEvent, + categoryEvent = categoryEvent, + categoryState = categoryState, + itemState = itemState, + itemEvent = itemEvent, ) }, drawerState = drawerState, @@ -209,8 +228,11 @@ private fun OverloadNavigationWrapper( drawerState.open() } }, - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, + filePickerLauncher = filePickerLauncher, ) } } @@ -228,22 +250,25 @@ fun OverloadAppContent( selectedDestination: String, navigateToTopLevelDestination: (OverloadTopLevelDestination) -> Unit, onDrawerClicked: () -> Unit = {}, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, + filePickerLauncher: ActivityResultLauncher, ) { var forgotDialogState by remember { mutableStateOf(false) } - LaunchedEffect(state.isForgotToStopDialogShown) { - forgotDialogState = state.isForgotToStopDialogShown + LaunchedEffect(itemState.isForgotToStopDialogShown) { + forgotDialogState = itemState.isForgotToStopDialogShown } var adjustEndDialogState by remember { mutableStateOf(false) } - LaunchedEffect(state.isAdjustEndDialogShown) { - adjustEndDialogState = state.isAdjustEndDialogShown + LaunchedEffect(itemState.isAdjustEndDialogShown) { + adjustEndDialogState = itemState.isAdjustEndDialogShown } var spreadAcrossDaysDialogState by remember { mutableStateOf(false) } - LaunchedEffect(state.isSpreadAcrossDaysDialogShown) { - spreadAcrossDaysDialogState = state.isSpreadAcrossDaysDialogShown + LaunchedEffect(itemState.isSpreadAcrossDaysDialogShown) { + spreadAcrossDaysDialogState = itemState.isSpreadAcrossDaysDialogShown } Row(modifier = modifier.fillMaxSize()) { @@ -253,8 +278,10 @@ fun OverloadAppContent( navigationContentPosition = navigationContentPosition, navigateToTopLevelDestination = navigateToTopLevelDestination, onDrawerClicked = onDrawerClicked, - state = state, - onEvent = onEvent, + categoryEvent = categoryEvent, + categoryState = categoryState, + itemState = itemState, + itemEvent = itemEvent, ) } Column( @@ -267,13 +294,16 @@ fun OverloadAppContent( navigationType = navigationType, contentType = contentType, navController = navController, - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, + filePickerLauncher = filePickerLauncher, modifier = Modifier .weight(1f) .then( - if (navigationType == OverloadNavigationType.BOTTOM_NAVIGATION && state.isDeletingHome.not()) { + if (navigationType == OverloadNavigationType.BOTTOM_NAVIGATION && itemState.isDeletingHome.not()) { Modifier.consumeWindowInsets( WindowInsets.systemBars.only( WindowInsetsSides.Bottom, @@ -288,33 +318,38 @@ fun OverloadAppContent( OverloadBottomNavigationBar( selectedDestination = selectedDestination, navigateToTopLevelDestination = navigateToTopLevelDestination, - state = state, - onEvent = onEvent, - onNavigate = { navController.navigate(OverloadRoute.CALENDAR) }, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, + navController = navController, ) } } } if (forgotDialogState) { ForgotToStopDialog( - onClose = { onEvent(ItemEvent.SetForgotToStopDialogShown(false)) }, - onEvent, + onClose = { itemEvent(ItemEvent.SetForgotToStopDialogShown(false)) }, + categoryState, + itemEvent, ) } if (adjustEndDialogState) { AdjustEndDialog( - onClose = { onEvent(ItemEvent.SetAdjustEndDialogShown(false)) }, - state, - onEvent, + onClose = { itemEvent(ItemEvent.SetAdjustEndDialogShown(false)) }, + categoryState, + itemState, + itemEvent, ) } if (spreadAcrossDaysDialogState) { SpreadAcrossDaysDialog( - onClose = { onEvent(ItemEvent.SetSpreadAcrossDaysDialogShown(false)) }, - state, - onEvent, + onClose = { itemEvent(ItemEvent.SetSpreadAcrossDaysDialogShown(false)) }, + categoryState, + itemState, + itemEvent, ) } } @@ -326,8 +361,11 @@ private fun OverloadNavHost( contentType: OverloadContentType, navController: NavHostController, modifier: Modifier = Modifier, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, + filePickerLauncher: ActivityResultLauncher, ) { NavHost( modifier = modifier, @@ -337,28 +375,46 @@ private fun OverloadNavHost( composable(OverloadRoute.HOME) { HomeTab( navigationType = navigationType, - state = state, - onEvent = onEvent, + categoryEvent = categoryEvent, + categoryState = categoryState, + itemState = itemState, + itemEvent = itemEvent, ) } composable(OverloadRoute.CALENDAR) { CalendarTab( contentType = contentType, - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, onNavigate = { navController.navigate(OverloadRoute.DAY) }, ) } + composable(OverloadRoute.CATEGORY) { + CategoryScreen( + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, + ) + } composable(OverloadRoute.DAY) { DayScreen( - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, ) } composable(OverloadRoute.CONFIGURATIONS) { ConfigurationsTab( - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, + filePickerLauncher = filePickerLauncher, + navController = navController, ) } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/TabItem.kt b/app/src/main/java/cloud/pablos/overload/ui/TabItem.kt index b2a28ae9..00dcc326 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/TabItem.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/TabItem.kt @@ -1,10 +1,11 @@ package cloud.pablos.overload.ui import androidx.compose.runtime.Composable +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState data class TabItem( val titleResId: Int, - val screen: @Composable (state: ItemState, onEvent: (ItemEvent) -> Unit) -> Unit, + val screen: @Composable (categoryState: CategoryState, itemState: ItemState, itemEvent: (ItemEvent) -> Unit) -> Unit, ) diff --git a/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationActions.kt b/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationActions.kt index bc0a839c..adb66bd2 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationActions.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationActions.kt @@ -15,6 +15,7 @@ import cloud.pablos.overload.R object OverloadRoute { const val HOME = "Home" const val CALENDAR = "Calendar" + const val CATEGORY = "Category" const val DAY = "Day" const val CONFIGURATIONS = "Configurations" } diff --git a/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationComponents.kt b/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationComponents.kt index 36cdc593..b0b3654b 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationComponents.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationComponents.kt @@ -47,17 +47,22 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset +import androidx.navigation.NavHostController import cloud.pablos.overload.R +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.Helpers.Companion.getSelectedDay +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState +import cloud.pablos.overload.ui.screens.category.CategoryScreenBottomAppBar +import cloud.pablos.overload.ui.screens.category.CategoryScreenTopAppBar import cloud.pablos.overload.ui.screens.day.DayScreenBottomAppBar import cloud.pablos.overload.ui.screens.day.DayScreenTopAppBar import cloud.pablos.overload.ui.tabs.calendar.CalendarTabTopAppBar import cloud.pablos.overload.ui.tabs.configurations.ConfigurationsTabTopAppBar import cloud.pablos.overload.ui.tabs.home.HomeTabDeleteBottomAppBar import cloud.pablos.overload.ui.tabs.home.HomeTabTopAppBar -import cloud.pablos.overload.ui.tabs.home.getItemsOfDay -import cloud.pablos.overload.ui.tabs.home.getSelectedDay import cloud.pablos.overload.ui.utils.OverloadNavigationContentPosition import cloud.pablos.overload.ui.views.DeleteTopAppBar import cloud.pablos.overload.ui.views.TextView @@ -69,8 +74,10 @@ fun OverloadNavigationRail( navigationContentPosition: OverloadNavigationContentPosition, navigateToTopLevelDestination: (OverloadTopLevelDestination) -> Unit, onDrawerClicked: () -> Unit = {}, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryEvent: (CategoryEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { NavigationRail( modifier = Modifier.fillMaxHeight(), @@ -85,7 +92,7 @@ fun OverloadNavigationRail( verticalArrangement = Arrangement.spacedBy(4.dp), ) { AnimatedVisibility( - visible = true, // state.isDeletingHome.not(), + visible = true, ) { NavigationRailItem( selected = false, @@ -99,13 +106,18 @@ fun OverloadNavigationRail( ) } - OverloadNavigationFabSmall(state = state, onEvent = onEvent) + OverloadNavigationFabSmall( + categoryEvent = categoryEvent, + categoryState = categoryState, + itemState = itemState, + itemEvent = itemEvent, + ) } Column(modifier = Modifier.layoutId(LayoutType.CONTENT)) { - when (state.isDeletingHome) { + when (itemState.isDeletingHome) { true -> { - val date = getSelectedDay(state) - val items = getItemsOfDay(date, state) + val date = getSelectedDay(itemState) + val items = getItems(categoryState, itemState, date) Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -114,11 +126,11 @@ fun OverloadNavigationRail( NavigationRailItem( selected = false, onClick = { - onEvent( + itemEvent( ItemEvent.SetSelectedItemsHome( - state.selectedItemsHome + + itemState.selectedItemsHome + items.filterNot { - state.selectedItemsHome.contains( + itemState.selectedItemsHome.contains( it, ) }, @@ -136,7 +148,7 @@ fun OverloadNavigationRail( NavigationRailItem( selected = false, onClick = { - onEvent(ItemEvent.SetSelectedItemsHome(state.selectedItemsHome - items.toSet())) + itemEvent(ItemEvent.SetSelectedItemsHome(itemState.selectedItemsHome - items.toSet())) }, icon = { Icon( @@ -149,7 +161,7 @@ fun OverloadNavigationRail( } false -> { - AnimatedVisibility(visible = state.isFabOpen.not()) { + AnimatedVisibility(visible = itemState.isFabOpen.not()) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp), @@ -190,32 +202,41 @@ class BottomBarState private constructor() { companion object { val Normal = BottomBarState() val Deleting = BottomBarState() + + val Category = BottomBarState() val Day = BottomBarState() } } +@RequiresApi(Build.VERSION_CODES.S) @Composable fun OverloadBottomNavigationBar( selectedDestination: String, navigateToTopLevelDestination: (OverloadTopLevelDestination) -> Unit, - state: ItemState, - onEvent: (ItemEvent) -> Unit, - onNavigate: () -> Unit, + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, + navController: NavHostController, ) { var currentBottomBarState by remember { mutableStateOf(BottomBarState.Normal) } - DisposableEffect(state.isDeletingHome, selectedDestination) { + DisposableEffect(itemState.isDeletingHome, selectedDestination) { currentBottomBarState = when (selectedDestination) { OverloadRoute.HOME -> { - when (state.isDeletingHome) { + when (itemState.isDeletingHome) { true -> BottomBarState.Deleting false -> BottomBarState.Normal } } + OverloadRoute.CATEGORY -> { + BottomBarState.Category + } + OverloadRoute.DAY -> { - when (state.isDeletingHome) { + when (itemState.isDeletingHome) { true -> BottomBarState.Deleting false -> BottomBarState.Day } @@ -230,8 +251,9 @@ fun OverloadBottomNavigationBar( for (bottomBarState in listOf( BottomBarState.Normal, - BottomBarState.Deleting, + BottomBarState.Category, BottomBarState.Day, + BottomBarState.Deleting, )) { AnimatedVisibility( visible = bottomBarState == currentBottomBarState, @@ -295,11 +317,15 @@ fun OverloadBottomNavigationBar( } BottomBarState.Deleting -> { - HomeTabDeleteBottomAppBar(state, onEvent) + HomeTabDeleteBottomAppBar(categoryState, itemState, itemEvent) + } + + BottomBarState.Category -> { + CategoryScreenBottomAppBar(categoryState, categoryEvent, itemState, itemEvent, navController) } BottomBarState.Day -> { - DayScreenBottomAppBar(onNavigate) + DayScreenBottomAppBar(navController) } } } @@ -312,43 +338,51 @@ class TopBarState private constructor() { val Calendar = TopBarState() val Configurations = TopBarState() + val Category = TopBarState() val Day = TopBarState() val Deleting = TopBarState() } } +@RequiresApi(Build.VERSION_CODES.S) @Composable fun OverloadTopAppBar( selectedDestination: String, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { var currentTopBarState by remember { mutableStateOf(TopBarState.Home) } - DisposableEffect(state.isDeletingHome, selectedDestination) { + DisposableEffect(itemState.isDeletingHome, selectedDestination) { currentTopBarState = when (selectedDestination) { OverloadRoute.HOME -> { - when (state.isDeletingHome) { + when (itemState.isDeletingHome) { true -> TopBarState.Deleting false -> TopBarState.Home } } OverloadRoute.CALENDAR -> { - when (state.isDeletingHome) { + when (itemState.isDeletingHome) { true -> TopBarState.Deleting false -> TopBarState.Calendar } } + OverloadRoute.CATEGORY -> { + TopBarState.Category + } + OverloadRoute.CONFIGURATIONS -> { TopBarState.Configurations } OverloadRoute.DAY -> { - when (state.isDeletingHome) { + when (itemState.isDeletingHome) { true -> TopBarState.Deleting false -> TopBarState.Day } @@ -369,11 +403,11 @@ fun OverloadTopAppBar( if (topBarState == currentTopBarState) { when (topBarState) { TopBarState.Home -> { - HomeTabTopAppBar() + HomeTabTopAppBar(categoryState, categoryEvent) } TopBarState.Calendar -> { - CalendarTabTopAppBar(state, onEvent) + CalendarTabTopAppBar(categoryState, categoryEvent, itemState, itemEvent) } TopBarState.Configurations -> { @@ -384,6 +418,7 @@ fun OverloadTopAppBar( } for (topBarState in listOf( + TopBarState.Category, TopBarState.Day, TopBarState.Deleting, )) { @@ -393,12 +428,16 @@ fun OverloadTopAppBar( exit = slideOutVertically(targetOffsetY = { -it }), ) { when (topBarState) { + TopBarState.Category -> { + CategoryScreenTopAppBar(categoryState) + } + TopBarState.Day -> { - DayScreenTopAppBar(state) + DayScreenTopAppBar(itemState) } TopBarState.Deleting -> { - DeleteTopAppBar(state, onEvent) + DeleteTopAppBar(itemState, itemEvent) } } } @@ -411,8 +450,10 @@ fun ModalNavigationDrawerContent( navigationContentPosition: OverloadNavigationContentPosition, navigateToTopLevelDestination: (OverloadTopLevelDestination) -> Unit, onDrawerClicked: () -> Unit = {}, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryEvent: (CategoryEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { ModalDrawerSheet { Layout( @@ -446,14 +487,14 @@ fun ModalNavigationDrawerContent( } } - OverloadNavigationFab(state, onEvent, onDrawerClicked) + OverloadNavigationFab(categoryEvent, categoryState, itemState, itemEvent, onDrawerClicked) } Column(modifier = Modifier.layoutId(LayoutType.CONTENT)) { - when (state.isDeletingHome) { + when (itemState.isDeletingHome) { true -> { - val date = getSelectedDay(state) - val items = getItemsOfDay(date, state) + val date = getSelectedDay(itemState) + val items = getItems(categoryState, itemState, date) Column( modifier = Modifier.verticalScroll(rememberScrollState()), @@ -462,11 +503,11 @@ fun ModalNavigationDrawerContent( NavigationDrawerItem( selected = false, onClick = { - onEvent( + itemEvent( ItemEvent.SetSelectedItemsHome( - state.selectedItemsHome + + itemState.selectedItemsHome + items.filterNot { - state.selectedItemsHome.contains( + itemState.selectedItemsHome.contains( it, ) }, @@ -494,7 +535,7 @@ fun ModalNavigationDrawerContent( NavigationDrawerItem( selected = false, onClick = { - onEvent(ItemEvent.SetSelectedItemsHome(state.selectedItemsHome - items.toSet())) + itemEvent(ItemEvent.SetSelectedItemsHome(itemState.selectedItemsHome - items.toSet())) }, label = { TextView( @@ -517,7 +558,7 @@ fun ModalNavigationDrawerContent( } } false -> { - AnimatedVisibility(visible = state.isFabOpen.not()) { + AnimatedVisibility(visible = itemState.isFabOpen.not()) { Column( modifier = Modifier.verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationFab.kt b/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationFab.kt index 9772af59..cac717ee 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationFab.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationFab.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Category import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.DeleteForever import androidx.compose.material.icons.filled.PlayArrow @@ -29,11 +30,15 @@ import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cloud.pablos.overload.R +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.Helpers.Companion.decideForeground +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState -import cloud.pablos.overload.data.item.startOrStopPause +import cloud.pablos.overload.data.item.fabPress import cloud.pablos.overload.ui.tabs.home.HomeTabManualDialog -import cloud.pablos.overload.ui.tabs.home.getItemsOfDay import cloud.pablos.overload.ui.views.TextView import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest @@ -41,13 +46,18 @@ import java.time.LocalDate @Composable fun OverloadNavigationFab( - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryEvent: (CategoryEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, onDrawerClicked: () -> Unit = {}, ) { + val backgroundColor = decideBackground(categoryState) + val foregroundColor = decideForeground(backgroundColor) + val date = LocalDate.now() - val itemsForToday = getItemsOfDay(date, state) + val itemsForToday = getItems(categoryState, itemState, date) val isOngoing = itemsForToday.isNotEmpty() && itemsForToday.last().ongoing @@ -67,8 +77,8 @@ fun OverloadNavigationFab( delay(viewConfiguration.longPressTimeoutMillis) isLongClick = true - onEvent(ItemEvent.SetIsFabOpen(true)) - onEvent(ItemEvent.SetIsDeletingHome(false)) + itemEvent(ItemEvent.SetIsFabOpen(true)) + itemEvent(ItemEvent.SetIsDeletingHome(false)) } is PressInteraction.Release -> { } @@ -83,11 +93,11 @@ fun OverloadNavigationFab( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp), ) { - when (state.isFabOpen) { + when (itemState.isFabOpen) { true -> { FloatingActionButton( onClick = { - onEvent(ItemEvent.SetIsFabOpen(false)) + itemEvent(ItemEvent.SetIsFabOpen(false)) }, interactionSource = interactionSource, containerColor = MaterialTheme.colorScheme.primaryContainer, @@ -119,12 +129,12 @@ fun OverloadNavigationFab( SmallFloatingActionButton( onClick = { - onEvent(ItemEvent.SetIsFabOpen(false)) + itemEvent(ItemEvent.SetIsFabOpen(false)) manualDialogState.value = true onDrawerClicked() }, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.primaryContainer, + containerColor = backgroundColor, + contentColor = foregroundColor, modifier = Modifier.padding(bottom = 10.dp).fillMaxWidth(), ) { Column( @@ -138,7 +148,7 @@ fun OverloadNavigationFab( ) { Icon( imageVector = Icons.Default.Add, - contentDescription = stringResource(id = R.string.close), + contentDescription = stringResource(id = R.string.manual_entry), modifier = Modifier.padding(8.dp), ) @@ -149,13 +159,46 @@ fun OverloadNavigationFab( } } } + + SmallFloatingActionButton( + onClick = { + itemEvent(ItemEvent.SetIsFabOpen(false)) + categoryEvent(CategoryEvent.SetIsSwitchCategoryDialogOpenHome(true)) + onDrawerClicked() + }, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.padding(bottom = 10.dp).fillMaxWidth(), + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.padding(8.dp), + ) { + Icon( + imageVector = Icons.Default.Category, + contentDescription = stringResource(id = R.string.switch_category), + modifier = Modifier.padding(8.dp), + ) + + TextView( + text = stringResource(id = R.string.switch_category), + modifier = Modifier.padding(end = 8.dp), + ) + } + } + } } false -> { - when (state.isDeletingHome) { + when (itemState.isDeletingHome) { true -> { FloatingActionButton( onClick = { - onEvent(ItemEvent.DeleteItems(state.selectedItemsHome)) + itemEvent(ItemEvent.DeleteItems(itemState.selectedItemsHome)) }, interactionSource = interactionSource, containerColor = MaterialTheme.colorScheme.errorContainer, @@ -189,12 +232,12 @@ fun OverloadNavigationFab( FloatingActionButton( onClick = { if (isLongClick.not()) { - startOrStopPause(state, onEvent) + fabPress(categoryState, categoryEvent, itemState, itemEvent) } }, interactionSource = interactionSource, - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = backgroundColor, + contentColor = foregroundColor, modifier = Modifier.fillMaxWidth(), ) { Column( @@ -240,6 +283,6 @@ fun OverloadNavigationFab( } if (manualDialogState.value) { - HomeTabManualDialog(onClose = { manualDialogState.value = false }, state, onEvent) + HomeTabManualDialog(onClose = { manualDialogState.value = false }, categoryState, itemState, itemEvent) } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationFabSmall.kt b/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationFabSmall.kt index c2bf42a6..ca2e1f93 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationFabSmall.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/navigation/OverloadNavigationFabSmall.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Category import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Stop @@ -24,24 +25,33 @@ import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cloud.pablos.overload.R +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.Helpers.Companion.decideForeground +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState -import cloud.pablos.overload.data.item.startOrStopPause +import cloud.pablos.overload.data.item.fabPress import cloud.pablos.overload.ui.tabs.home.HomeTabDeleteFAB import cloud.pablos.overload.ui.tabs.home.HomeTabManualDialog -import cloud.pablos.overload.ui.tabs.home.getItemsOfDay import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import java.time.LocalDate @Composable fun OverloadNavigationFabSmall( - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryEvent: (CategoryEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { + val backgroundColor = decideBackground(categoryState) + val foregroundColor = decideForeground(backgroundColor) + val date = LocalDate.now() - val itemsForToday = getItemsOfDay(date, state) + val itemsForToday = getItems(categoryState, itemState, date) val isOngoing = itemsForToday.isNotEmpty() && itemsForToday.last().ongoing @@ -61,8 +71,8 @@ fun OverloadNavigationFabSmall( delay(viewConfiguration.longPressTimeoutMillis) isLongClick = true - onEvent(ItemEvent.SetIsFabOpen(true)) - onEvent(ItemEvent.SetIsDeletingHome(false)) + itemEvent(ItemEvent.SetIsFabOpen(true)) + itemEvent(ItemEvent.SetIsDeletingHome(false)) } is PressInteraction.Release -> { } @@ -77,11 +87,11 @@ fun OverloadNavigationFabSmall( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp), ) { - when (state.isFabOpen) { + when (itemState.isFabOpen) { true -> { FloatingActionButton( onClick = { - onEvent(ItemEvent.SetIsFabOpen(false)) + itemEvent(ItemEvent.SetIsFabOpen(false)) }, containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer, @@ -94,34 +104,48 @@ fun OverloadNavigationFabSmall( SmallFloatingActionButton( onClick = { - onEvent(ItemEvent.SetIsFabOpen(false)) + itemEvent(ItemEvent.SetIsFabOpen(false)) manualDialogState.value = true }, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.primaryContainer, + containerColor = backgroundColor, + contentColor = foregroundColor, ) { Icon( imageVector = Icons.Default.Add, contentDescription = stringResource(id = R.string.manual_entry), ) } + + SmallFloatingActionButton( + onClick = { + itemEvent(ItemEvent.SetIsFabOpen(false)) + categoryEvent(CategoryEvent.SetIsSwitchCategoryDialogOpenHome(true)) + }, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) { + Icon( + imageVector = Icons.Default.Category, + contentDescription = stringResource(id = R.string.switch_category), + ) + } } false -> { - when (state.isDeletingHome) { + when (itemState.isDeletingHome) { true -> { - HomeTabDeleteFAB(state, onEvent) + HomeTabDeleteFAB(itemState, itemEvent) } false -> { FloatingActionButton( onClick = { if (isLongClick.not()) { - startOrStopPause(state, onEvent) + fabPress(categoryState, categoryEvent, itemState, itemEvent) } }, interactionSource = interactionSource, - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = backgroundColor, + contentColor = foregroundColor, ) { Icon( imageVector = @@ -145,6 +169,6 @@ fun OverloadNavigationFabSmall( } if (manualDialogState.value) { - HomeTabManualDialog(onClose = { manualDialogState.value = false }, state, onEvent) + HomeTabManualDialog(onClose = { manualDialogState.value = false }, categoryState, itemState, itemEvent) } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreen.kt b/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreen.kt new file mode 100644 index 00000000..20e95f32 --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreen.kt @@ -0,0 +1,350 @@ +package cloud.pablos.overload.ui.screens.category + +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DarkMode +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import cloud.pablos.overload.R +import cloud.pablos.overload.data.Converters.Companion.convertColorToLong +import cloud.pablos.overload.data.Converters.Companion.convertLongToColor +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState +import cloud.pablos.overload.data.item.ItemEvent +import cloud.pablos.overload.data.item.ItemState +import cloud.pablos.overload.ui.navigation.OverloadRoute +import cloud.pablos.overload.ui.navigation.OverloadTopAppBar +import cloud.pablos.overload.ui.tabs.configurations.ConfigurationDescription +import cloud.pablos.overload.ui.tabs.configurations.ConfigurationTitle +import cloud.pablos.overload.ui.tabs.configurations.ConfigurationsTabItem +import cloud.pablos.overload.ui.tabs.configurations.HoDivider +import cloud.pablos.overload.ui.tabs.configurations.SelectableColor +import cloud.pablos.overload.ui.tabs.configurations.SelectableEmoji +import cloud.pablos.overload.ui.tabs.configurations.colorOptions +import cloud.pablos.overload.ui.tabs.configurations.emojiOptions + +@RequiresApi(Build.VERSION_CODES.S) +@Composable +fun CategoryScreen( + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, +) { + val selectedCategory = categoryState.categories.find { it.id == categoryState.selectedCategoryConfigurations } + var name by remember { mutableStateOf(TextFieldValue(selectedCategory?.name ?: "")) } + var color by remember { mutableStateOf(selectedCategory?.color?.let { convertLongToColor(it) } ?: Color.Unspecified) } + var emoji by remember { mutableStateOf(selectedCategory?.emoji ?: "") } + + var nameError by remember { mutableStateOf(false) } + + LaunchedEffect(color, emoji) { + if (selectedCategory != null) { + save( + categoryEvent, + selectedCategory.id, + name.text, + selectedCategory.goal1, + selectedCategory.goal2, + convertColorToLong(color), + emoji, + selectedCategory.isDefault, + ) + } + } + + val goalDialogState = remember { mutableStateOf(false) } + val pauseGoalDialogState = remember { mutableStateOf(false) } + + if (selectedCategory != null) { + Scaffold( + topBar = { + OverloadTopAppBar( + selectedDestination = OverloadRoute.CATEGORY, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, + ) + }, + ) { paddingValues -> + LazyColumn( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + item { + ConfigurationsTabItem(title = "Name") + } + + item { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + OutlinedTextField( + value = name, + onValueChange = { + name = it + + if (it.text.isNotEmpty()) { + nameError = false + } + }, + singleLine = true, + placeholder = { Text(text = "Name") }, + isError = nameError, + keyboardActions = + KeyboardActions( + onDone = { + if (name.text.isEmpty()) { + nameError = true + return@KeyboardActions + } else { + nameError = false + } + + save( + categoryEvent, + selectedCategory.id, + name.text, + selectedCategory.goal1, + selectedCategory.goal2, + convertColorToLong(color), + emoji, + selectedCategory.isDefault, + ) + }, + ), + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + if (selectedCategory.isDefault.not()) { + item { + HoDivider() + } + + item { + ConfigurationsTabItem(title = "Color") + } + + item { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Row( + modifier = + Modifier + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + colorOptions.forEach { colorOption -> + SelectableColor( + selected = colorOption == color, + onClick = { color = colorOption }, + color = colorOption, + surfaceColor = MaterialTheme.colorScheme.surfaceVariant, + ) + } + } + } + } + } + + item { + HoDivider() + } + + item { + ConfigurationsTabItem(title = "Emoji") + } + + item { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Row( + modifier = + Modifier + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + emojiOptions.forEach { emojiOption -> + SelectableEmoji( + selected = emojiOption == emoji, + onClick = { emoji = emojiOption }, + emoji = emojiOption, + color = color, + surfaceColor = MaterialTheme.colorScheme.surfaceVariant, + ) + } + } + } + } + + item { + HoDivider() + } + + item { + ConfigurationsTabItem(title = stringResource(id = R.string.goals)) + } + + // Goal 1 + item { + val itemLabel = stringResource(id = R.string.work) + ": " + stringResource(id = R.string.work_goal_descr) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .padding(bottom = 16.dp) + .clip(shape = RoundedCornerShape(15.dp)) + .clickable { + pauseGoalDialogState.value = true + } + .clearAndSetSemantics { + contentDescription = itemLabel + }, + ) { + Text( + selectedCategory.emoji, + modifier = + Modifier + .width(40.dp) + .padding(horizontal = 8.dp), + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + Column { + ConfigurationTitle(selectedCategory.name) + ConfigurationDescription("Set a goal for " + selectedCategory.name) + } + } + } + } + + // Goal 2 + item { + val itemLabel = stringResource(id = R.string.pause) + ": " + stringResource(id = R.string.pause_goal_descr) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .padding(bottom = 16.dp) + .clip(shape = RoundedCornerShape(15.dp)) + .clickable { + pauseGoalDialogState.value = true + } + .clearAndSetSemantics { + contentDescription = itemLabel + }, + ) { + Icon( + imageVector = Icons.Filled.DarkMode, + contentDescription = stringResource(id = R.string.pause), + tint = MaterialTheme.colorScheme.primary, + modifier = + Modifier + .width(40.dp) + .padding(horizontal = 8.dp), + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + Column { + ConfigurationTitle(stringResource(id = R.string.pause)) + ConfigurationDescription(stringResource(id = R.string.pause_goal_descr)) + } + } + } + } + } + } + + if (goalDialogState.value) { + CategoryScreenGoalDialog( + onClose = { goalDialogState.value = false }, + isPause = false, + category = selectedCategory, + categoryEvent = categoryEvent, + ) + } + + if (pauseGoalDialogState.value) { + CategoryScreenGoalDialog( + onClose = { pauseGoalDialogState.value = false }, + isPause = true, + category = selectedCategory, + categoryEvent = categoryEvent, + ) + } + } +} + +fun save( + categoryEvent: (CategoryEvent) -> Unit, + id: Int, + name: String, + goal1: Int, + goal2: Int, + color: Long, + emoji: String, + isDefault: Boolean, +) { + categoryEvent(CategoryEvent.SetId(id)) + categoryEvent(CategoryEvent.SetName(name)) + categoryEvent(CategoryEvent.SetColor(color)) + categoryEvent(CategoryEvent.SetGoal1(goal1)) + categoryEvent(CategoryEvent.SetGoal2(goal2)) + categoryEvent(CategoryEvent.SetEmoji(emoji)) + categoryEvent(CategoryEvent.SetIsDefault(isDefault)) + categoryEvent(CategoryEvent.SaveCategory) + + Log.d("category save", "yeeeeah weird") +} diff --git a/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenBottomAppBar.kt b/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenBottomAppBar.kt new file mode 100644 index 00000000..c403fc9c --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenBottomAppBar.kt @@ -0,0 +1,76 @@ +package cloud.pablos.overload.ui.screens.category + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowLeft +import androidx.compose.material.icons.filled.DeleteForever +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavHostController +import cloud.pablos.overload.R +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState +import cloud.pablos.overload.data.item.ItemEvent +import cloud.pablos.overload.data.item.ItemState +import cloud.pablos.overload.ui.navigation.OverloadRoute + +@RequiresApi(Build.VERSION_CODES.S) +@Composable +fun CategoryScreenBottomAppBar( + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, + navController: NavHostController, +) { + val selectedCategory = categoryState.categories.find { it.id == categoryState.selectedCategoryConfigurations } + val deleteCategoryDialog = remember { mutableStateOf(false) } + + BottomAppBar( + actions = { + IconButton(onClick = { navController.navigate(OverloadRoute.CONFIGURATIONS) }) { + Icon( + Icons.AutoMirrored.Outlined.KeyboardArrowLeft, + contentDescription = "Go Back", + ) + } + }, + floatingActionButton = { + FloatingActionButton( + onClick = { deleteCategoryDialog.value = true }, + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ) { + Icon( + Icons.Filled.DeleteForever, + contentDescription = stringResource(id = R.string.delete_items_forever), + ) + } + }, + ) + + if (deleteCategoryDialog.value && selectedCategory != null) { + CategoryScreenDeleteCategoryDialog( + onDismiss = { deleteCategoryDialog.value = false }, + onConfirm = { + categoryEvent(CategoryEvent.DeleteCategory(selectedCategory)) + + val items = getItems(categoryState, itemState) + itemEvent(ItemEvent.DeleteItems(items)) + + deleteCategoryDialog.value = false + navController.navigate(OverloadRoute.CONFIGURATIONS) + }, + category = selectedCategory, + ) + } +} diff --git a/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenDeleteCategoryDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenDeleteCategoryDialog.kt new file mode 100644 index 00000000..812aa14c --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenDeleteCategoryDialog.kt @@ -0,0 +1,96 @@ +package cloud.pablos.overload.ui.screens.category + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Dangerous +import androidx.compose.material3.AlertDialog +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.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import cloud.pablos.overload.R +import cloud.pablos.overload.data.category.Category +import cloud.pablos.overload.ui.views.TextView + +@RequiresApi(Build.VERSION_CODES.S) +@Composable +fun CategoryScreenDeleteCategoryDialog( + onDismiss: () -> Unit, + onConfirm: () -> Unit, + category: Category, +) { + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + imageVector = Icons.Rounded.Dangerous, + contentDescription = "Delete Category", + tint = MaterialTheme.colorScheme.error, + ) + }, + title = { + TextView( + text = "Delete Category", + fontWeight = FontWeight.Bold, + align = TextAlign.Center, + maxLines = 2, + modifier = Modifier.fillMaxWidth(), + ) + }, + text = { + Column { + Text( + text = + stringResource( + R.string.delete_category_warning, + category.name, + ), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + overflow = TextOverflow.Ellipsis, + ) + } + }, + confirmButton = { + Button( + onClick = { + onConfirm() + }, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ), + ) { + TextView(stringResource(R.string.yes)) + } + }, + dismissButton = { + Button( + onClick = { + onDismiss() + }, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ), + ) { + TextView(stringResource(R.string.no)) + } + }, + modifier = Modifier.padding(16.dp), + ) +} diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabGoalDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenGoalDialog.kt similarity index 78% rename from app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabGoalDialog.kt rename to app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenGoalDialog.kt index 33f1ce9c..27809666 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabGoalDialog.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenGoalDialog.kt @@ -1,4 +1,4 @@ -package cloud.pablos.overload.ui.tabs.configurations +package cloud.pablos.overload.ui.screens.category import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -26,13 +26,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import cloud.pablos.overload.R +import cloud.pablos.overload.data.category.Category +import cloud.pablos.overload.data.category.CategoryEvent import cloud.pablos.overload.ui.views.TextView @Composable -fun ConfigurationsTabGoalDialog( +fun CategoryScreenGoalDialog( + category: Category, + categoryEvent: (CategoryEvent) -> Unit, onClose: () -> Unit, isPause: Boolean, ) { @@ -46,7 +49,7 @@ fun ConfigurationsTabGoalDialog( val minFocusRequest = remember { FocusRequester() } val context = LocalContext.current - val sharedPreferences = remember { OlSharedPreferences(context) } + // val sharedPreferences = remember { OlSharedPreferences(context) } LaunchedEffect(Unit) { hoursFocusRequest.requestFocus() @@ -60,9 +63,7 @@ fun ConfigurationsTabGoalDialog( if (isPause) { stringResource(id = R.string.pick_pause_goal) } else { - stringResource( - id = R.string.pick_work_goal, - ) + "Set Goal" }, fontWeight = FontWeight.Bold, align = TextAlign.Center, @@ -95,11 +96,12 @@ fun ConfigurationsTabGoalDialog( Button( onClick = { onClose.save( - sharedPreferences = sharedPreferences, hours = hours.toIntOrNull(), minutes = minutes.toIntOrNull(), valid = hoursValidator && minValidator, isPause = isPause, + category = category, + categoryEvent = categoryEvent, ) }, colors = @@ -144,12 +146,13 @@ fun TimeInput( placeholder = { Text(text = "0") }, isError = isError, modifier = Modifier.focusRequester(focusRequester), - trailingIcon = { Text(text = label) }, + trailingIcon = { Text(text = label, modifier = Modifier.padding(end = 10.dp)) }, ) } private fun (() -> Unit).save( - sharedPreferences: OlSharedPreferences, + category: Category, + categoryEvent: (CategoryEvent) -> Unit, hours: Int?, minutes: Int?, valid: Boolean, @@ -161,17 +164,31 @@ private fun (() -> Unit).save( val goal = (hoursInMin + minutesInMin) * 60 * 1000 if (goal > 0) { - when (isPause) { - true -> sharedPreferences.savePauseGoal(goal) - false -> sharedPreferences.saveWorkGoal(goal) - } + saveGoal(category, categoryEvent, goal, isPause) this() } } } -@Preview -@Composable -fun ConfigurationsTabPauseGoalPreview() { - ConfigurationsTabGoalDialog(onClose = {}, isPause = true) +fun saveGoal( + category: Category, + categoryEvent: (CategoryEvent) -> Unit, + goal: Int, + isPause: Boolean, +) { + categoryEvent(CategoryEvent.SetId(category.id)) + categoryEvent(CategoryEvent.SetName(category.name)) + categoryEvent(CategoryEvent.SetColor(category.color)) + categoryEvent(CategoryEvent.SetEmoji(category.emoji)) + categoryEvent(CategoryEvent.SetIsDefault(category.isDefault)) + + if (isPause) { + categoryEvent(CategoryEvent.SetGoal1(category.goal1)) + categoryEvent(CategoryEvent.SetGoal2(goal)) + } else { + categoryEvent(CategoryEvent.SetGoal1(goal)) + categoryEvent(CategoryEvent.SetGoal2(category.goal2)) + } + + categoryEvent(CategoryEvent.SaveCategory) } diff --git a/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenTopAppBar.kt b/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenTopAppBar.kt new file mode 100644 index 00000000..1bb9a832 --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/ui/screens/category/CategoryScreenTopAppBar.kt @@ -0,0 +1,30 @@ +package cloud.pablos.overload.ui.screens.category + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import cloud.pablos.overload.data.category.CategoryState +import cloud.pablos.overload.ui.views.TextView + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CategoryScreenTopAppBar(categoryState: CategoryState) { + val selectedCategory = categoryState.categories.find { it.id == categoryState.selectedCategoryConfigurations } + + Surface( + tonalElevation = NavigationBarDefaults.Elevation, + color = MaterialTheme.colorScheme.background, + ) { + TopAppBar( + title = { + TextView( + text = "Category" + ": " + (selectedCategory?.name ?: "Unknown"), + fontSize = MaterialTheme.typography.titleLarge.fontSize, + ) + }, + ) + } +} diff --git a/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreen.kt b/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreen.kt index 1d946203..2b41d1d8 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreen.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreen.kt @@ -16,13 +16,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import cloud.pablos.overload.data.Converters.Companion.convertStringToLocalDateTime +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState import cloud.pablos.overload.ui.navigation.OverloadRoute import cloud.pablos.overload.ui.navigation.OverloadTopAppBar import cloud.pablos.overload.ui.views.DayScreenDayView import cloud.pablos.overload.ui.views.getLocalDate -import cloud.pablos.overload.ui.views.parseToLocalDateTime import java.time.LocalDate import java.time.temporal.ChronoUnit @@ -30,17 +33,21 @@ import java.time.temporal.ChronoUnit @RequiresApi(Build.VERSION_CODES.S) @Composable fun DayScreen( - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { - val selectedDay = getLocalDate(state.selectedDayCalendar) + val selectedDay = getLocalDate(itemState.selectedDayCalendar) + + val items = getItems(categoryState, itemState) val firstYear = - if (state.items.isEmpty()) { + if (items.isEmpty()) { LocalDate.now().year } else { - state.items.minByOrNull { it.startTime } - ?.let { parseToLocalDateTime(it.startTime).year } + items.minByOrNull { it.startTime } + ?.let { convertStringToLocalDateTime(it.startTime).year } ?: LocalDate.now().year } @@ -56,7 +63,7 @@ fun DayScreen( ) LaunchedEffect(pagerState.currentPage) { - onEvent( + itemEvent( ItemEvent.SetSelectedDayCalendar( LocalDate.now() .minusDays((daysCount - pagerState.currentPage - 1).toLong()) @@ -68,7 +75,7 @@ fun DayScreen( var hasLoaded by remember { mutableStateOf(false) } LaunchedEffect(hasLoaded) { if (!hasLoaded) { - if (getLocalDate(state.selectedDayCalendar) != LocalDate.now()) { + if (getLocalDate(itemState.selectedDayCalendar) != LocalDate.now()) { pagerState.scrollToPage(ChronoUnit.DAYS.between(firstDay, selectedDay).toInt()) } hasLoaded = true @@ -79,8 +86,10 @@ fun DayScreen( topBar = { OverloadTopAppBar( selectedDestination = OverloadRoute.DAY, - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, ) }, ) { paddingValues -> @@ -93,8 +102,9 @@ fun DayScreen( DayScreenDayView( daysCount = daysCount, page = page, - state = state, - onEvent = onEvent, + categoryState = categoryState, + itemState = itemState, + itemEvent = itemEvent, ) } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreenBottomAppBar.kt b/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreenBottomAppBar.kt index 891b45f2..c9d7e6f9 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreenBottomAppBar.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreenBottomAppBar.kt @@ -6,12 +6,14 @@ import androidx.compose.material3.BottomAppBar import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import cloud.pablos.overload.ui.navigation.OverloadRoute @Composable -fun DayScreenBottomAppBar(onNavigate: () -> Unit) { +fun DayScreenBottomAppBar(navController: NavHostController) { BottomAppBar( actions = { - IconButton(onClick = { onNavigate() }) { + IconButton(onClick = { navController.navigate(OverloadRoute.CALENDAR) }) { Icon( Icons.AutoMirrored.Outlined.KeyboardArrowLeft, contentDescription = "Go Back", diff --git a/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreenTopAppBar.kt b/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreenTopAppBar.kt index ab265fb1..45f3f6a5 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreenTopAppBar.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/screens/day/DayScreenTopAppBar.kt @@ -13,8 +13,8 @@ import cloud.pablos.overload.ui.views.getLocalDate @OptIn(ExperimentalMaterial3Api::class) @Composable -fun DayScreenTopAppBar(state: ItemState) { - val selectedDay = getLocalDate(state.selectedDayCalendar) +fun DayScreenTopAppBar(itemState: ItemState) { + val selectedDay = getLocalDate(itemState.selectedDayCalendar) val title = getFormattedDate(selectedDay, true) Surface( diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTab.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTab.kt index 8e4d27ae..f25a0109 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTab.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTab.kt @@ -24,11 +24,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import cloud.pablos.overload.data.Converters.Companion.convertStringToLocalDateTime +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState import cloud.pablos.overload.ui.navigation.OverloadRoute @@ -39,7 +42,6 @@ import cloud.pablos.overload.ui.views.DayScreenDayView import cloud.pablos.overload.ui.views.TextView import cloud.pablos.overload.ui.views.YearView import cloud.pablos.overload.ui.views.getLocalDate -import cloud.pablos.overload.ui.views.parseToLocalDateTime import java.time.LocalDate import java.time.temporal.ChronoUnit @@ -48,30 +50,83 @@ import java.time.temporal.ChronoUnit @Composable fun CalendarTab( contentType: OverloadContentType, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, onNavigate: () -> Unit, ) { - val selectedYear by remember { mutableIntStateOf(state.selectedYearCalendar) } - - LaunchedEffect(selectedYear) { - if (state.selectedYearCalendar != LocalDate.now().year) { - onEvent(ItemEvent.SetSelectedYearCalendar(LocalDate.now().year)) - } - } - Scaffold( topBar = { OverloadTopAppBar( selectedDestination = OverloadRoute.CALENDAR, - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, ) }, ) { paddingValues -> Box(modifier = Modifier.fillMaxSize()) { + val selectedYear by remember { mutableIntStateOf(itemState.selectedYearCalendar) } + val selectedDay = getLocalDate(itemState.selectedDayCalendar) + val items = getItems(categoryState, itemState) + + LaunchedEffect(selectedYear) { + if (itemState.selectedYearCalendar != selectedDay.year) { + itemEvent(ItemEvent.SetSelectedYearCalendar(selectedDay.year)) + } + } + Column(modifier = Modifier.padding(paddingValues)) { AnimatedVisibility(visible = contentType == OverloadContentType.DUAL_PANE) { + val firstYear = + if (items.isEmpty()) { + LocalDate.now().year + } else { + items.minByOrNull { it.startTime } + ?.let { convertStringToLocalDateTime(it.startTime).year } + ?: LocalDate.now().year + } + val firstDay = LocalDate.of(firstYear, 1, 1) + val lastDay = LocalDate.now() + + val daysCount = ChronoUnit.DAYS.between(firstDay, lastDay).toInt() + 1 + + var scrollToPage = true + val pagerState = + rememberPagerState( + initialPage = daysCount, + initialPageOffsetFraction = 0f, + pageCount = { daysCount }, + ) + + LaunchedEffect(pagerState.currentPage) { + scrollToPage = false + itemEvent( + ItemEvent.SetSelectedDayCalendar( + LocalDate.now() + .minusDays((daysCount - pagerState.currentPage - 1).toLong()) + .toString(), + ), + ) + } + + LaunchedEffect(itemState.selectedDayCalendar) { + if (scrollToPage) { + val highlightedDay = LocalDate.now().minusDays((daysCount - pagerState.currentPage - 1).toLong()) + if (getLocalDate(itemState.selectedDayCalendar) != highlightedDay) { + pagerState.scrollToPage(ChronoUnit.DAYS.between(firstDay, selectedDay).toInt()) + } + } else { + scrollToPage = true + } + + if (selectedYear != selectedDay.year) { + itemEvent(ItemEvent.SetSelectedYearCalendar(selectedDay.year)) + } + } + Row(modifier = Modifier.fillMaxSize()) { Box( modifier = Modifier.weight(1f), @@ -85,9 +140,10 @@ fun CalendarTab( } YearView( - onEvent = onEvent, - date = getLocalDate(state.selectedDayCalendar), - year = state.selectedYearCalendar, + categoryState = categoryState, + itemEvent = itemEvent, + date = getLocalDate(itemState.selectedDayCalendar), + year = itemState.selectedYearCalendar, bottomPadding = 0.dp, highlightSelectedDay = true, ) @@ -97,56 +153,6 @@ fun CalendarTab( Box( modifier = Modifier.weight(1f), ) { - val selectedDay = getLocalDate(state.selectedDayCalendar) - - val firstYear = - if (state.items.isEmpty()) { - LocalDate.now().year - } else { - state.items.minByOrNull { it.startTime } - ?.let { parseToLocalDateTime(it.startTime).year } - ?: LocalDate.now().year - } - - val firstDay = LocalDate.of(firstYear, 1, 1) - val lastDay = LocalDate.now() - val daysCount = ChronoUnit.DAYS.between(firstDay, lastDay).toInt() + 1 - - var scrollToPage = true - - val pagerState = - rememberPagerState( - initialPage = daysCount, - initialPageOffsetFraction = 0f, - pageCount = { daysCount }, - ) - - LaunchedEffect(pagerState.currentPage) { - scrollToPage = false - onEvent( - ItemEvent.SetSelectedDayCalendar( - LocalDate.now() - .minusDays((daysCount - pagerState.currentPage - 1).toLong()) - .toString(), - ), - ) - - if (selectedYear != selectedDay.year) { - onEvent(ItemEvent.SetSelectedYearCalendar(selectedDay.year)) - } - } - - LaunchedEffect(state.selectedDayCalendar) { - if (scrollToPage) { - val highlightedDay = LocalDate.now().minusDays((daysCount - pagerState.currentPage - 1).toLong()) - if (getLocalDate(state.selectedDayCalendar) != highlightedDay) { - pagerState.scrollToPage(ChronoUnit.DAYS.between(firstDay, selectedDay).toInt()) - } - } else { - scrollToPage = true - } - } - HorizontalPager( state = pagerState, ) { page -> @@ -164,8 +170,9 @@ fun CalendarTab( DayScreenDayView( daysCount = daysCount, page = page, - state = state, - onEvent = onEvent, + categoryState = categoryState, + itemState = itemState, + itemEvent = itemEvent, ) } } @@ -182,9 +189,11 @@ fun CalendarTab( } YearView( - onEvent = onEvent, - date = getLocalDate(state.selectedDayCalendar), - year = state.selectedYearCalendar, + categoryState = categoryState, + itemEvent = itemEvent, + date = getLocalDate(itemState.selectedDayCalendar), + year = itemState.selectedYearCalendar, + bottomPadding = 16.dp, onNavigate = onNavigate, ) } diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTabTopAppBar.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTabTopAppBar.kt index 7e295378..bb1a41f3 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTabTopAppBar.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTabTopAppBar.kt @@ -1,40 +1,32 @@ package cloud.pablos.overload.ui.tabs.calendar -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.Surface import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import cloud.pablos.overload.R +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState +import cloud.pablos.overload.ui.views.ChangeCategoryButton +import cloud.pablos.overload.ui.views.ChangeYearButton import cloud.pablos.overload.ui.views.TextView -import cloud.pablos.overload.ui.views.parseToLocalDateTime -import java.time.LocalDate +@RequiresApi(Build.VERSION_CODES.S) @OptIn(ExperimentalMaterial3Api::class) @Composable fun CalendarTabTopAppBar( - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { - val yearDialogState = remember { mutableStateOf(false) } - - val firstYear = if (state.items.isEmpty()) { - LocalDate.now().year - } else { - state.items.minByOrNull { it.startTime }?.let { parseToLocalDateTime(it.startTime).year } ?: LocalDate.now().year - } - val yearsCount = LocalDate.now().year - firstYear - Surface( tonalElevation = NavigationBarDefaults.Elevation, color = MaterialTheme.colorScheme.background, @@ -47,21 +39,8 @@ fun CalendarTabTopAppBar( ) }, actions = { - if (yearsCount > 0) { - Button( - onClick = { yearDialogState.value = true }, - modifier = Modifier.padding(horizontal = 8.dp), - ) { - TextView(state.selectedYearCalendar.toString()) - } - if (yearDialogState.value) { - CalendarTabYearDialog( - firstYear = firstYear, - onEvent = onEvent, - onClose = { yearDialogState.value = false }, - ) - } - } + ChangeYearButton(itemState, itemEvent) + ChangeCategoryButton(categoryState, categoryEvent) }, ) } diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTabYearDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTabYearDialog.kt index 032f811d..13c3a5dd 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTabYearDialog.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/calendar/CalendarTabYearDialog.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -30,7 +29,7 @@ import java.time.LocalDate @Composable fun CalendarTabYearDialog( firstYear: Int, - onEvent: (ItemEvent) -> Unit, + itemEvent: (ItemEvent) -> Unit, onClose: () -> Unit, ) { Dialog( @@ -42,7 +41,7 @@ fun CalendarTabYearDialog( color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxWidth(), ) { - YearDialogContent(firstYear = firstYear, onEvent = onEvent, onClose = onClose) + YearDialogContent(firstYear = firstYear, itemEvent = itemEvent, onClose = onClose) } }, ) @@ -51,7 +50,7 @@ fun CalendarTabYearDialog( @Composable private fun YearDialogContent( firstYear: Int, - onEvent: (ItemEvent) -> Unit, + itemEvent: (ItemEvent) -> Unit, onClose: () -> Unit, ) { Column( @@ -71,22 +70,20 @@ private fun YearDialogContent( modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), ) - YearListContent(firstYear = firstYear, onEvent = onEvent, onClose = onClose) + YearListContent(firstYear = firstYear, itemEvent = itemEvent, onClose = onClose) } } @Composable private fun YearListContent( firstYear: Int, - onEvent: (ItemEvent) -> Unit, + itemEvent: (ItemEvent) -> Unit, onClose: () -> Unit, ) { - LazyColumn( - modifier = Modifier.fillMaxHeight(), - ) { + LazyColumn { val currentYear = LocalDate.now().year items((currentYear downTo firstYear).toList()) { year -> - YearRow(year = year, onEvent = onEvent, onClose = onClose) + YearRow(year = year, itemEvent = itemEvent, onClose = onClose) if (year != firstYear) { HorizontalDivider() } @@ -95,15 +92,20 @@ private fun YearListContent( } @Composable -private fun YearRow(year: Int, onEvent: (ItemEvent) -> Unit, onClose: () -> Unit) { +private fun YearRow( + year: Int, + itemEvent: (ItemEvent) -> Unit, + onClose: () -> Unit, +) { Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onEvent(ItemEvent.SetSelectedYearCalendar(year)) - onClose() - } - .padding(16.dp), + modifier = + Modifier + .fillMaxWidth() + .clickable { + itemEvent(ItemEvent.SetSelectedYearCalendar(year)) + onClose() + } + .padding(16.dp), horizontalArrangement = Arrangement.Center, ) { TextView(text = year.toString()) diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTab.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTab.kt index c57b7022..a686cc89 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTab.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTab.kt @@ -1,24 +1,27 @@ package cloud.pablos.overload.ui.tabs.configurations +import android.app.Activity import android.content.ContentResolver import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Build import android.preference.PreferenceManager +import android.util.Log import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandIn import androidx.compose.animation.shrinkOut -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DarkMode +import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.rounded.Archive import androidx.compose.material.icons.rounded.BugReport import androidx.compose.material.icons.rounded.Code @@ -27,7 +30,7 @@ import androidx.compose.material.icons.rounded.EmojiNature import androidx.compose.material.icons.rounded.PestControl import androidx.compose.material.icons.rounded.Translate import androidx.compose.material.icons.rounded.Unarchive -import androidx.compose.material.icons.rounded.Work +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -38,34 +41,43 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider -import androidx.core.content.edit import androidx.core.net.toUri import androidx.lifecycle.LifecycleCoroutineScope +import androidx.navigation.NavHostController import androidx.room.withTransaction import cloud.pablos.overload.R +import cloud.pablos.overload.data.Backup +import cloud.pablos.overload.data.OverloadDatabase +import cloud.pablos.overload.data.category.Category +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.Item -import cloud.pablos.overload.data.item.ItemDatabase import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState -import cloud.pablos.overload.data.item.backupItemsToCsv +import cloud.pablos.overload.ui.MainActivity import cloud.pablos.overload.ui.navigation.OverloadRoute import cloud.pablos.overload.ui.navigation.OverloadTopAppBar import cloud.pablos.overload.ui.views.TextView +import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File +@RequiresApi(Build.VERSION_CODES.S) @Composable fun ConfigurationsTab( - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, + filePickerLauncher: ActivityResultLauncher, + navController: NavHostController, ) { val context = LocalContext.current val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) @@ -86,16 +98,16 @@ fun ConfigurationsTab( mutableStateOf(sharedPreferences.getBoolean(acraSysLogsEnabledKey, true)) } - val importDialogState = remember { mutableStateOf(false) } - val workGoalDialogState = remember { mutableStateOf(false) } - val pauseGoalDialogState = remember { mutableStateOf(false) } + val createCategoryDialog = remember { mutableStateOf(false) } Scaffold( topBar = { OverloadTopAppBar( selectedDestination = OverloadRoute.CONFIGURATIONS, - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, ) }, ) { paddingValues -> @@ -105,87 +117,57 @@ fun ConfigurationsTab( .fillMaxSize() .padding(paddingValues) .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), ) { - // Goals Title + // Categories Title item { - ConfigurationsTabItem(title = stringResource(id = R.string.goals)) + ConfigurationsTabItem(title = stringResource(id = R.string.categories)) } - // Work Goal - item { - val itemLabel = - stringResource(id = R.string.work) + ": " + stringResource(id = R.string.work_goal_descr) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier - .clickable { - workGoalDialogState.value = true - } - .clearAndSetSemantics { - contentDescription = itemLabel - }, - ) { - Icon( - imageVector = Icons.Rounded.Work, - contentDescription = stringResource(id = R.string.work), - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 8.dp), + categoryState.categories.forEach { category -> + item { + ConfigurationsTabItem( + title = category.emoji + " " + category.name, + action = { + categoryEvent(CategoryEvent.SetSelectedCategoryConfigurations(category.id)) + navController.navigate(OverloadRoute.CATEGORY) + }, + background = true, ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth(), - ) { - Column { - ConfigurationTitle(stringResource(id = R.string.work)) - ConfigurationDescription(stringResource(id = R.string.work_goal_descr)) - } - } } } - // Pause Goal item { - val itemLabel = - stringResource(id = R.string.pause) + ": " + stringResource(id = R.string.pause_goal_descr) - Row( - verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, modifier = Modifier - .clickable { - pauseGoalDialogState.value = true - } - .clearAndSetSemantics { - contentDescription = itemLabel - }, + .fillMaxWidth(), ) { - Icon( - imageVector = Icons.Filled.DarkMode, - contentDescription = stringResource(id = R.string.pause), - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 8.dp), - ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth(), - ) { - Column { - ConfigurationTitle(stringResource(id = R.string.pause)) - ConfigurationDescription(stringResource(id = R.string.pause_goal_descr)) + FilledTonalButton(onClick = { + createCategoryDialog.value = true + }) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = stringResource(id = R.string.select_year), + tint = MaterialTheme.colorScheme.onSecondaryContainer, + ) + TextView( + text = stringResource(id = R.string.add_category), + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) } } } } - // Goals Divider + // Categories Divider item { - Row { - HorizontalDivider() - } + HoDivider() } // Analytics Title @@ -223,9 +205,7 @@ fun ConfigurationsTab( // Analytics Divider item { - Row { - HorizontalDivider() - } + HoDivider() } // Storage Title @@ -233,81 +213,27 @@ fun ConfigurationsTab( ConfigurationsTabItem(title = stringResource(id = R.string.storage)) } - // Storage Backup item { - val itemLabel = - stringResource(id = R.string.backup) + ": " + stringResource(id = R.string.backup_descr) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier - .clickable { - backup(state, context) - } - .clearAndSetSemantics { - contentDescription = itemLabel - }, - ) { - Icon( - imageVector = Icons.Rounded.Archive, - contentDescription = stringResource(id = R.string.backup), - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 8.dp), - ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth(), - ) { - Column { - ConfigurationTitle(stringResource(id = R.string.backup)) - ConfigurationDescription(stringResource(id = R.string.backup_descr)) - } - } - } + ConfigurationsTabItem( + title = stringResource(id = R.string.backup), + description = stringResource(id = R.string.backup_descr), + icon = Icons.Rounded.Archive, + action = { backup(categoryState, itemState, context) }, + ) } - // Storage Import item { - val itemLabel = - stringResource(id = R.string.import_ol) + ": " + stringResource(id = R.string.import_descr) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier - .clickable { - importDialogState.value = true - } - .clearAndSetSemantics { - contentDescription = itemLabel - }, - ) { - Icon( - imageVector = Icons.Rounded.Unarchive, - contentDescription = stringResource(id = R.string.import_ol), - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 8.dp), - ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth(), - ) { - Column { - ConfigurationTitle(stringResource(id = R.string.import_ol)) - ConfigurationDescription(stringResource(id = R.string.import_descr)) - } - } - } + ConfigurationsTabItem( + title = stringResource(id = R.string.import_ol), + description = stringResource(id = R.string.import_descr), + icon = Icons.Rounded.Unarchive, + action = { launchFilePicker(filePickerLauncher) }, + ) } // Storage Divider item { - Row { - HorizontalDivider() - } + HoDivider() } // About Title @@ -320,7 +246,7 @@ fun ConfigurationsTab( ConfigurationsTabItem( title = stringResource(id = R.string.sourcecode), description = stringResource(id = R.string.sourcecode_descr), - link = "https://codeberg.org/pabloscloud/Overload".toUri(), + link = "https://github.com/pabloscloud/Overload".toUri(), icon = Icons.Rounded.Code, ) } @@ -330,7 +256,7 @@ fun ConfigurationsTab( ConfigurationsTabItem( title = stringResource(id = R.string.issue_reports), description = stringResource(id = R.string.issue_reports_descr), - link = "https://codeberg.org/pabloscloud/Overload/issues".toUri(), + link = "https://github.com/pabloscloud/Overload/issues".toUri(), icon = Icons.Rounded.EmojiNature, ) } @@ -340,7 +266,7 @@ fun ConfigurationsTab( ConfigurationsTabItem( title = stringResource(id = R.string.translate), description = stringResource(id = R.string.translate_descr), - link = "https://translate.codeberg.org/engage/overload".toUri(), + link = "https://crowdin.com/project/overload".toUri(), icon = Icons.Rounded.Translate, ) } @@ -350,16 +276,14 @@ fun ConfigurationsTab( ConfigurationsTabItem( title = stringResource(id = R.string.license), description = stringResource(id = R.string.license_descr), - link = "https://codeberg.org/pabloscloud/Overload/raw/branch/main/LICENSE".toUri(), + link = "https://github.com/pabloscloud/Overload/blob/main/LICENSE".toUri(), icon = Icons.Rounded.Copyright, ) } // About Divider item { - Row { - HorizontalDivider() - } + HoDivider() } // Footer @@ -369,39 +293,30 @@ fun ConfigurationsTab( modifier = Modifier .fillMaxWidth() - .padding(bottom = 16.dp), + .padding(horizontal = 5.dp), ) { ConfigurationDescription(stringResource(id = R.string.footer)) } } } - if (importDialogState.value) { - ConfigurationsTabImportDialog(onClose = { importDialogState.value = false }) - } - if (workGoalDialogState.value) { - ConfigurationsTabGoalDialog( - onClose = { workGoalDialogState.value = false }, - isPause = false, - ) - } - - if (pauseGoalDialogState.value) { - ConfigurationsTabGoalDialog( - onClose = { pauseGoalDialogState.value = false }, - isPause = true, + if (createCategoryDialog.value) { + ConfigurationsTabCreateCategoryDialog( + onClose = { createCategoryDialog.value = false }, + categoryEvent = categoryEvent, ) } } } fun backup( - state: ItemState, + categoryState: CategoryState, + itemState: ItemState, context: Context, ) { try { - val exportedData = backupItemsToCsv(state) - val cachePath = File(context.cacheDir, "backup.csv") + val exportedData = Backup.backupToJson(categoryState, itemState) + val cachePath = File(context.cacheDir, "backup.json") cachePath.writeText(exportedData) @@ -416,7 +331,7 @@ fun backup( Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_STREAM, contentUri) - type = "text/comma-separated-values" + type = "application/json" } val shareIntent = Intent.createChooser(sendIntent, null) @@ -428,10 +343,19 @@ fun backup( } } +fun launchFilePicker(filePickerLauncher: ActivityResultLauncher) { + val intent = + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/json" + } + filePickerLauncher.launch(intent) +} + fun handleIntent( intent: Intent?, lifecycleScope: LifecycleCoroutineScope, - db: ItemDatabase, + db: OverloadDatabase, context: Context, contentResolver: ContentResolver, ) { @@ -449,12 +373,12 @@ fun handleIntent( } else if (intent.getStringExtra(Intent.EXTRA_STREAM)?.isBlank() == false) { val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) uri?.let { - importFile(uri, contentResolver, context, db, lifecycleScope) + importCsvFile(uri, contentResolver, context, db, lifecycleScope) } } else if (intent.clipData != null) { val uri = intent.clipData?.getItemAt(0)?.uri if (uri != null) { - importFile(uri, contentResolver, context, db, lifecycleScope) + importCsvFile(uri, contentResolver, context, db, lifecycleScope) } else { showImportFailedToast(context) } @@ -467,7 +391,7 @@ fun handleIntent( private fun importCsvData( csvData: String, lifecycleScope: LifecycleCoroutineScope, - db: ItemDatabase, + db: OverloadDatabase, context: Context, ) { val parsedData = parseCsvData(csvData) @@ -491,6 +415,7 @@ private fun importCsvData( endTime = endTime, ongoing = ongoing.toBoolean(), pause = pause.toBoolean(), + categoryId = 1, ) val importResult = itemDao.upsertItem(item) @@ -504,6 +429,7 @@ private fun importCsvData( withContext(Dispatchers.Main) { if (allImportsSucceeded) { showImportSuccessToast(context) + restartApp(context) } else { showImportFailedToast(context) } @@ -511,6 +437,89 @@ private fun importCsvData( } } +private fun importJsonData( + jsonData: String, + lifecycleScope: LifecycleCoroutineScope, + db: OverloadDatabase, + context: Context, +) { + lifecycleScope.launch(Dispatchers.IO) { + try { + val gson = Gson() + val databaseBackup = gson.fromJson(jsonData, Backup.DatabaseBackup::class.java) + + when (databaseBackup.backupVersion) { + 2 -> { + Log.d("import", "Import started") + val itemDao = db.itemDao() + val categoryDao = db.categoryDao() + + var allImportsSucceeded = true + + db.withTransaction { + Log.d("import", "Importing items") + val itemsTable = databaseBackup.data["items"] ?: emptyList() + itemsTable.forEach { itemData -> + val item = + Item( + id = (itemData["id"] as? Double)?.toInt() ?: 0, + startTime = itemData["startTime"] as String, + endTime = itemData["endTime"] as String, + ongoing = itemData["ongoing"] as Boolean, + pause = itemData["pause"] as Boolean, + categoryId = (itemData["categoryId"] as? Double)?.toInt() ?: 0, + ) + + val importResult = itemDao.upsertItem(item) + if (importResult != Unit) { + allImportsSucceeded = false + } + } + + val categoriesTable = databaseBackup.data["categories"] ?: emptyList() + categoriesTable.forEach { categoriesData -> + val category = + Category( + id = (categoriesData["id"] as? Double)?.toInt() ?: 0, + color = (categoriesData["color"] as? Double)?.toLong() ?: 0, + emoji = categoriesData["emoji"] as String, + goal1 = (categoriesData["goal1"] as? Double)?.toInt() ?: 0, + goal2 = (categoriesData["goal2"] as? Double)?.toInt() ?: 0, + isDefault = categoriesData["isDefault"] as Boolean, + name = categoriesData["name"] as String, + ) + + val importResult = categoryDao.upsertCategory(category) + if (importResult != Unit) { + allImportsSucceeded = false + } + } + } + + withContext(Dispatchers.Main) { + if (allImportsSucceeded) { + showImportSuccessToast(context) + restartApp(context) + } else { + showImportFailedToast(context) + } + } + } + else -> { + withContext(Dispatchers.Main) { + showImportFailedToast(context) + } + } + } + } catch (e: Exception) { + Log.d("import", e.toString()) + withContext(Dispatchers.Main) { + showImportFailedToast(context) + } + } + } +} + fun parseCsvData(csvData: String): List> { val rows = csvData.split("\n") return rows.map { row -> @@ -533,11 +542,11 @@ fun showImportFailedToast(context: Context) { Toast.makeText(context, context.getString(R.string.import_failure), Toast.LENGTH_SHORT).show() } -private fun importFile( +fun importCsvFile( uri: Uri, contentResolver: ContentResolver, context: Context, - db: ItemDatabase, + db: OverloadDatabase, lifecycleScope: LifecycleCoroutineScope, ) { uri.let { @@ -549,21 +558,41 @@ private fun importFile( } } +fun importJsonFile( + uri: Uri, + contentResolver: ContentResolver, + context: Context, + db: OverloadDatabase, + lifecycleScope: LifecycleCoroutineScope, +) { + uri.let { + contentResolver.openInputStream(uri)?.use { inputStream -> + val sharedData = inputStream.bufferedReader().readText() + + importJsonData(sharedData, lifecycleScope, db, context) + } + } +} + @Composable fun ConfigurationLabel(text: String) { TextView( text = text, fontSize = MaterialTheme.typography.titleLarge.fontSize, color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(vertical = 15.dp), ) } @Composable -fun ConfigurationTitle(text: String) { +fun ConfigurationTitle( + text: String, + color: Color = MaterialTheme.colorScheme.onBackground, +) { TextView( text = text, fontSize = MaterialTheme.typography.titleMedium.fontSize, - color = MaterialTheme.colorScheme.onBackground, + color = color, ) } @@ -576,18 +605,20 @@ fun ConfigurationDescription(text: String) { ) } +@Composable +fun HoDivider() { + HorizontalDivider(modifier = Modifier.padding(top = 20.dp)) +} + class OlSharedPreferences(context: Context) { private val sharedPreferences = context.getSharedPreferences("ol_prefs", Context.MODE_PRIVATE) +} - fun saveWorkGoal(goal: Int) { - sharedPreferences.edit { putInt("workGoal", goal) } - } - - fun savePauseGoal(goal: Int) { - sharedPreferences.edit { putInt("pauseGoal", goal) } +fun restartApp(context: Context) { + val intent = Intent(context.applicationContext, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + context.startActivity(intent) + if (context is Activity) { + context.finish() } - - fun getWorkGoal(): Int = sharedPreferences.getInt("workGoal", 0) - - fun getPauseGoal(): Int = sharedPreferences.getInt("pauseGoal", 0) } diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabCreateCategoryDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabCreateCategoryDialog.kt new file mode 100644 index 00000000..af929a55 --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabCreateCategoryDialog.kt @@ -0,0 +1,383 @@ +/* + * This portion of code is derived from the Read You app, which is licensed under GNU GPLv3. + * Original copyright (c) 2022 Ashinch. + * + * The portions of code are used under the terms of the GNU GPLv3 license. + * See https://www.gnu.org/licenses/gpl-3.0.html for more details. + * + * Modifications: + * - colors + * - size + * - layout + * - content + */ + +package cloud.pablos.overload.ui.tabs.configurations + +import androidx.compose.animation.Animatable +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandIn +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkOut +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +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.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import cloud.pablos.overload.R +import cloud.pablos.overload.data.Converters.Companion.convertColorToLong +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.ui.views.TextView + +val colorOptions: List = + listOf( + Color(255, 209, 220), // Pastel Pink + Color(255, 204, 204), // Pastel Red + Color(250, 223, 173), // Pastel Yellow + Color(255, 230, 204), // Light Orange + Color(204, 255, 229), // Pastel Green + Color(230, 255, 204), // Light Lime + Color(221, 204, 255), // Pastel Purple + Color(230, 204, 255), // Light Indigo + Color(204, 230, 255), // Pastel Blue + Color(204, 255, 255), // Light Cyan + Color(255, 204, 230), // Light Magenta + Color(255, 230, 255), // Light Lavender + ) + +val emojiOptions: List = + listOf( + "💼", + "👔", + "💻", + "🖋️", + "📚", + "🎓", + "📝", + "✏️", + "🏋️‍♂️", + "🚴", + "🏃", + "⛹️‍♀️", + "🎉", + "🍻", + "🎮", + "🍹", + "👨‍👩‍👧‍👦", + "👪", + "🏡", + "🎨", + "🎸", + "🎮", + "📷", + "🍳", + "🍔", + "🍕", + "🥗", + "✈️", + "🚗", + "🚢", + "🌍", + "💊", + "🧘", + "🏥", + "🌱", + "🛀", + "🌅", + "🛋️", + "📺", + ) + +@Composable +fun ConfigurationsTabCreateCategoryDialog( + onClose: () -> Unit, + categoryEvent: (CategoryEvent) -> Unit, +) { + var name by remember { mutableStateOf(TextFieldValue()) } + var color by remember { mutableStateOf(colorOptions.first()) } + var emoji by remember { mutableStateOf(emojiOptions.first()) } + + var nameError by remember { mutableStateOf(false) } + + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + AlertDialog( + onDismissRequest = onClose, + title = { + TextView( + text = "Create Category", + fontWeight = FontWeight.Bold, + align = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + }, + text = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + TextView( + "Name", + fontWeight = FontWeight.Bold, + color = if (nameError) MaterialTheme.colorScheme.error else Color.Unspecified, + ) + OutlinedTextField( + value = name, + onValueChange = + { + name = it + if (it.text.isNotEmpty()) { + nameError = false + } + }, + singleLine = true, + placeholder = { Text(text = "Name") }, + isError = nameError, + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), + ) + } + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + TextView( + "Color", + fontWeight = FontWeight.Bold, + ) + Row( + modifier = + Modifier + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + colorOptions.forEach { colorOption -> + SelectableColor( + selected = colorOption == color, + onClick = { color = colorOption }, + color = colorOption, + ) + } + } + } + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + TextView( + "Emoji", + fontWeight = FontWeight.Bold, + ) + Row( + modifier = + Modifier + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + emojiOptions.forEach { emojiOption -> + SelectableEmoji( + selected = emojiOption == emoji, + onClick = { emoji = emojiOption }, + emoji = emojiOption, + color = color, + ) + } + } + } + } + }, + confirmButton = { + Button( + onClick = { + if (name.text.isEmpty()) { + nameError = true + return@Button + } else { + nameError = false + } + + categoryEvent(CategoryEvent.SetName(name.text.replaceFirstChar { it.uppercase() })) + categoryEvent(CategoryEvent.SetEmoji(emoji)) + categoryEvent(CategoryEvent.SetColor(convertColorToLong(color))) + categoryEvent(CategoryEvent.SetIsDefault(false)) + categoryEvent(CategoryEvent.SaveCategory) + onClose() + }, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + ) { + TextView(stringResource(id = R.string.save)) + } + }, + dismissButton = { + Button( + onClick = onClose, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ), + ) { + TextView(text = stringResource(R.string.cancel)) + } + }, + modifier = Modifier.padding(16.dp), + ) +} + +@Composable +fun SelectableColor( + modifier: Modifier = Modifier, + selected: Boolean, + onClick: () -> Unit, + color: Color, + surfaceColor: Color = MaterialTheme.colorScheme.surfaceContainerLowest, +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + color = surfaceColor, + ) { + Surface( + modifier = + Modifier + .clickable { onClick() } + .padding(12.dp) + .size(34.dp), + shape = CircleShape, + color = color, + ) { + Box { + AnimatedVisibility( + visible = selected, + modifier = + Modifier + .align(Alignment.Center) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary), + enter = fadeIn() + expandIn(expandFrom = Alignment.Center), + exit = shrinkOut(shrinkTowards = Alignment.Center) + fadeOut(), + ) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = "Checked", + modifier = + Modifier + .padding(5.dp) + .size(15.dp), + tint = MaterialTheme.colorScheme.surface, + ) + } + } + } + } +} + +@Composable +fun SelectableEmoji( + modifier: Modifier = Modifier, + selected: Boolean, + onClick: () -> Unit, + emoji: String, + color: Color, + surfaceColor: Color = MaterialTheme.colorScheme.surfaceContainerLowest, +) { + val surfaceColorSelected = MaterialTheme.colorScheme.primary + val surfaceColorBySelection = remember { Animatable(if (selected) surfaceColorSelected else surfaceColor) } + LaunchedEffect(selected) { + surfaceColorBySelection.animateTo( + if (selected) surfaceColorSelected else surfaceColor, + animationSpec = tween(250), + ) + } + + val bgColor = remember { Animatable(color) } + LaunchedEffect(color) { + bgColor.animateTo( + color, + animationSpec = tween(250), + ) + } + + Surface( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + color = surfaceColorBySelection.value, + ) { + Surface( + modifier = + Modifier + .clickable { onClick() } + .padding(12.dp) + .size(34.dp), + shape = CircleShape, + color = Color.LightGray, + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .clip(CircleShape) + .background(bgColor.value) + .fillMaxSize(), + ) { + Text(emoji) + } + } + } +} diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabImportDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabImportDialog.kt deleted file mode 100644 index 85fc1619..00000000 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabImportDialog.kt +++ /dev/null @@ -1,98 +0,0 @@ -package cloud.pablos.overload.ui.tabs.configurations - -import android.content.Intent -import androidx.compose.foundation.layout.Column -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.text.ClickableText -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Info -import androidx.compose.material3.AlertDialog -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.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import androidx.core.net.toUri -import cloud.pablos.overload.R -import cloud.pablos.overload.ui.views.TextView - -@Composable -fun ConfigurationsTabImportDialog(onClose: () -> Unit) { - val context = LocalContext.current - val learnMoreLink = "https://codeberg.org/pabloscloud/Overload#import-backup".toUri() - - AlertDialog( - onDismissRequest = onClose, - icon = { - Icon( - imageVector = Icons.Rounded.Info, - contentDescription = stringResource(id = R.string.import_backup), - tint = MaterialTheme.colorScheme.primary, - ) - }, - title = { - TextView( - text = stringResource(id = R.string.import_backup), - fontWeight = FontWeight.Bold, - align = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) - }, - text = { - Column { - Text( - text = stringResource(id = R.string.import_backup_descr), - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - overflow = TextOverflow.Ellipsis, - ) - Spacer(modifier = Modifier.height(16.dp)) - - val openLinkStr = stringResource(id = R.string.open_link_with) - ClickableText( - text = AnnotatedString(stringResource(id = R.string.learn_more)), - style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.primary, textAlign = TextAlign.Center), - onClick = { - val intent = Intent(Intent.ACTION_VIEW, learnMoreLink) - val chooserIntent = Intent.createChooser(intent, openLinkStr) - ContextCompat.startActivity(context, chooserIntent, null) - }, - modifier = Modifier.fillMaxWidth(), - ) - } - }, - confirmButton = { - Button( - onClick = { onClose() }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), - modifier = Modifier.fillMaxWidth(), - ) { - TextView(stringResource(id = R.string.close)) - } - }, - modifier = Modifier.padding(16.dp), - ) -} - -@Preview -@Composable -fun ConfigurationsTabImportDialogPreview() { - ConfigurationsTabImportDialog {} -} diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabItem.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabItem.kt index 491429b5..bc105f70 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabItem.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/configurations/ConfigurationsTabItem.kt @@ -4,21 +4,27 @@ import android.content.Intent import android.content.SharedPreferences import android.net.Uri import android.preference.PreferenceManager +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos import androidx.compose.material.icons.rounded.Info import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -37,6 +43,8 @@ fun ConfigurationsTabItem( icon: ImageVector? = null, preferenceKey: String? = null, switchState: MutableState? = null, + action: (() -> Unit)? = null, + background: Boolean = false, ) { val context = LocalContext.current @@ -45,37 +53,83 @@ fun ConfigurationsTabItem( verticalAlignment = Alignment.CenterVertically, modifier = if (link != null) { - Modifier.clickable { - val intent = Intent(Intent.ACTION_VIEW, link) - val chooserIntent = Intent.createChooser(intent, openLinkStr) - context.startActivity(chooserIntent) - } + Modifier + .clip(shape = RoundedCornerShape(15.dp)) + .clickable { + val intent = Intent(Intent.ACTION_VIEW, link) + val chooserIntent = Intent.createChooser(intent, openLinkStr) + context.startActivity(chooserIntent) + } + .padding(vertical = 10.dp) .clearAndSetSemantics { contentDescription = "$title: $description" } + } else if (background && action != null) { + val shape = RoundedCornerShape(15.dp) + Modifier + .clip(shape) + .background(MaterialTheme.colorScheme.surfaceVariant, shape) + .clickable { + action() + } + .padding(10.dp) + } else if (action != null) { + val shape = RoundedCornerShape(15.dp) + Modifier + .clip(shape) + .clickable { + action() + } + .padding(vertical = 10.dp) } else { - Modifier.clearAndSetSemantics { - contentDescription = "$title: $description" - } + Modifier + .clearAndSetSemantics { + contentDescription = "$title: $description" + } }, ) { - if (icon != null) { - Icon( - imageVector = icon, - contentDescription = title, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 8.dp), - ) - } - if (description != null) { + if (description != null || background) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(15.dp), modifier = Modifier.fillMaxWidth(), ) { - Column(modifier = Modifier.weight(1f)) { - ConfigurationTitle(title) - ConfigurationDescription(description) + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = title, + tint = + if (!background) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = + if (!background) { + Modifier.padding(horizontal = 15.dp) + } else { + Modifier + }, + ) + } + if (description != null) { + Column(modifier = Modifier.weight(1f)) { + ConfigurationTitle(title) + ConfigurationDescription(description) + } + } else { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + ConfigurationTitle(title, MaterialTheme.colorScheme.onSurfaceVariant) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos, + contentDescription = title, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } if (preferenceKey != null && switchState != null) { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) @@ -91,7 +145,7 @@ fun ConfigurationsTabItem( } } } else { - Row(Modifier.padding(top = 16.dp)) { + Row { ConfigurationLabel(title.replaceFirstChar { it.uppercase() }) } } @@ -118,10 +172,15 @@ fun AcraSwitch( @Preview @Composable fun ConfigurationsTabItemPreview() { - ConfigurationsTabItem( - title = "F-Droid", - description = "please support them", - link = "https://f-droid.org".toUri(), - icon = Icons.Rounded.Info, - ) + Surface( + tonalElevation = NavigationBarDefaults.Elevation, + color = MaterialTheme.colorScheme.background, + ) { + ConfigurationsTabItem( + title = "Overload Website", + description = "click here to open the website", + link = "https://overload.pablos.cloud".toUri(), + icon = Icons.Rounded.Info, + ) + } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTab.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTab.kt index a86240f1..e3879f3c 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTab.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTab.kt @@ -22,17 +22,24 @@ import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState import cloud.pablos.overload.ui.navigation.OverloadRoute import cloud.pablos.overload.ui.navigation.OverloadTopAppBar +import cloud.pablos.overload.ui.tabs.configurations.ConfigurationsTabCreateCategoryDialog import cloud.pablos.overload.ui.utils.OverloadNavigationType +import cloud.pablos.overload.ui.views.SwitchCategoryDialog import cloud.pablos.overload.ui.views.TextView import kotlinx.coroutines.launch import java.time.LocalDate @@ -44,9 +51,13 @@ import java.time.format.DateTimeFormatter @Composable fun HomeTab( navigationType: OverloadNavigationType, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryEvent: (CategoryEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { + val backgroundColor = decideBackground(categoryState) + val pagerState = rememberPagerState( initialPage = 2, @@ -64,27 +75,29 @@ fun HomeTab( 2 -> getFormattedDate() else -> getFormattedDate() } - onEvent(ItemEvent.SetSelectedDayCalendar(selectedDayString)) + itemEvent(ItemEvent.SetSelectedDayCalendar(selectedDayString)) } Scaffold( topBar = { OverloadTopAppBar( selectedDestination = OverloadRoute.HOME, - state = state, - onEvent = onEvent, + categoryState = categoryState, + categoryEvent = categoryEvent, + itemState = itemState, + itemEvent = itemEvent, ) }, floatingActionButton = { AnimatedVisibility( visible = navigationType == OverloadNavigationType.BOTTOM_NAVIGATION && - state.selectedDayCalendar == LocalDate.now().toString() && - state.isDeletingHome.not(), - enter = if (state.isFabOpen) slideInHorizontally(initialOffsetX = { w -> w }) else scaleIn(), - exit = if (state.isFabOpen) slideOutHorizontally(targetOffsetX = { w -> w }) else scaleOut(), + itemState.selectedDayCalendar == LocalDate.now().toString() && + itemState.isDeletingHome.not(), + enter = if (itemState.isFabOpen) slideInHorizontally(initialOffsetX = { w -> w }) else scaleIn(), + exit = if (itemState.isFabOpen) slideOutHorizontally(targetOffsetX = { w -> w }) else scaleOut(), ) { - HomeTabFab(state = state, onEvent = onEvent) + HomeTabFab(categoryEvent = categoryEvent, categoryState = categoryState, itemState = itemState, itemEvent = itemEvent) } }, ) { paddingValues -> @@ -97,6 +110,13 @@ fun HomeTab( PrimaryTabRow( selectedTabIndex = pagerState.currentPage, divider = {}, + indicator = { + TabRowDefaults.PrimaryIndicator( + modifier = Modifier.tabIndicatorOffset(pagerState.currentPage, matchContentSize = true), + width = Dp.Unspecified, + color = backgroundColor, + ) + }, ) { homeTabItems.forEachIndexed { index, item -> Tab( @@ -120,11 +140,30 @@ fun HomeTab( state = pagerState, ) { page -> val item = homeTabItems[page] - item.screen(state, onEvent) + item.screen(categoryState, itemState, itemEvent) } } } } + + if (categoryState.isCreateCategoryDialogOpenHome) { + ConfigurationsTabCreateCategoryDialog( + onClose = { + categoryEvent(CategoryEvent.SetIsCreateCategoryDialogOpenHome(false)) + }, + categoryEvent, + ) + } + + if (categoryState.isSwitchCategoryDialogOpenHome) { + SwitchCategoryDialog( + categoryState, + categoryEvent, + onClose = { + categoryEvent(CategoryEvent.SetIsSwitchCategoryDialogOpenHome(false)) + }, + ) + } } fun getFormattedDate( diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabDeleteBottomAppBar.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabDeleteBottomAppBar.kt index 6429eebe..e6d2363f 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabDeleteBottomAppBar.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabDeleteBottomAppBar.kt @@ -12,31 +12,30 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import cloud.pablos.overload.R -import cloud.pablos.overload.data.item.Item +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.Helpers.Companion.getSelectedDay +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState -import cloud.pablos.overload.ui.views.extractDate -import cloud.pablos.overload.ui.views.getLocalDate -import cloud.pablos.overload.ui.views.parseToLocalDateTime -import java.time.LocalDate @Composable fun HomeTabDeleteBottomAppBar( - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { - val date = getSelectedDay(state) + val date = getSelectedDay(itemState) - val itemsForSelectedDay = getItemsOfDay(date, state) + val itemsForSelectedDay = getItems(categoryState, itemState, date) BottomAppBar( actions = { IconButton(onClick = { - onEvent( + itemEvent( ItemEvent.SetSelectedItemsHome( - state.selectedItemsHome + + itemState.selectedItemsHome + itemsForSelectedDay.filterNot { - state.selectedItemsHome.contains( + itemState.selectedItemsHome.contains( it, ) }, @@ -49,7 +48,7 @@ fun HomeTabDeleteBottomAppBar( ) } IconButton(onClick = { - onEvent(ItemEvent.SetSelectedItemsHome(state.selectedItemsHome - itemsForSelectedDay.toSet())) + itemEvent(ItemEvent.SetSelectedItemsHome(itemState.selectedItemsHome - itemsForSelectedDay.toSet())) }) { Icon( Icons.Filled.Deselect, @@ -60,7 +59,7 @@ fun HomeTabDeleteBottomAppBar( floatingActionButton = { FloatingActionButton( onClick = { - onEvent(ItemEvent.DeleteItems(state.selectedItemsHome)) + itemEvent(ItemEvent.DeleteItems(itemState.selectedItemsHome)) }, containerColor = MaterialTheme.colorScheme.errorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer, @@ -76,12 +75,12 @@ fun HomeTabDeleteBottomAppBar( @Composable fun HomeTabDeleteFAB( - state: ItemState, - onEvent: (ItemEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { FloatingActionButton( onClick = { - onEvent(ItemEvent.DeleteItems(state.selectedItemsHome)) + itemEvent(ItemEvent.DeleteItems(itemState.selectedItemsHome)) }, containerColor = MaterialTheme.colorScheme.errorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer, @@ -92,18 +91,3 @@ fun HomeTabDeleteFAB( ) } } - -fun getSelectedDay(state: ItemState): LocalDate { - return state.selectedDayCalendar.takeIf { it.isNotBlank() }?.let { getLocalDate(it) } - ?: LocalDate.now() -} - -fun getItemsOfDay( - date: LocalDate, - state: ItemState, -): List { - return state.items.filter { item -> - val startTime = parseToLocalDateTime(item.startTime) - extractDate(startTime) == date - } -} diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabDeletePauseDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabDeletePauseDialog.kt index 957fd01b..b889080c 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabDeletePauseDialog.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabDeletePauseDialog.kt @@ -33,7 +33,7 @@ import cloud.pablos.overload.ui.views.TextView @Composable fun HomeTabDeletePauseDialog(onClose: () -> Unit) { val context = LocalContext.current - val learnMoreLink = "https://codeberg.org/pabloscloud/Overload#delete-pause".toUri() + val learnMoreLink = "https://github.com/pabloscloud/Overload?tab=readme-ov-file#why-cant-i-delete-an-ongoing-pause".toUri() AlertDialog( onDismissRequest = onClose, @@ -65,7 +65,11 @@ fun HomeTabDeletePauseDialog(onClose: () -> Unit) { val openLinkStr = stringResource(id = R.string.open_link_with) ClickableText( text = AnnotatedString(stringResource(id = R.string.learn_more)), - style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.primary, textAlign = TextAlign.Center), + style = + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, + ), onClick = { val intent = Intent(Intent.ACTION_VIEW, learnMoreLink) val chooserIntent = Intent.createChooser(intent, openLinkStr) @@ -79,10 +83,11 @@ fun HomeTabDeletePauseDialog(onClose: () -> Unit) { Button( onClick = { onClose() }, modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), ) { TextView(stringResource(id = R.string.close)) } diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabEditItemDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabEditItemDialog.kt index f9bbc024..330b3eb4 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabEditItemDialog.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabEditItemDialog.kt @@ -36,11 +36,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import cloud.pablos.overload.R +import cloud.pablos.overload.data.Converters.Companion.convertStringToLocalDateTime +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.Item import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState import cloud.pablos.overload.ui.views.TextView -import cloud.pablos.overload.ui.views.parseToLocalDateTime import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -49,8 +51,9 @@ import java.util.Calendar @Composable fun HomeTabEditItemDialog( onClose: () -> Unit, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { val context = LocalContext.current @@ -68,17 +71,17 @@ fun HomeTabEditItemDialog( var selectedItem: Item? = null - if (state.selectedItemsHome.size == 1) { - selectedItem = state.selectedItemsHome.first() + if (itemState.selectedItemsHome.size == 1) { + selectedItem = itemState.selectedItemsHome.first() - selectedStart = parseToLocalDateTime(selectedItem.startTime) - selectedEnd = parseToLocalDateTime(selectedItem.endTime) + selectedStart = convertStringToLocalDateTime(selectedItem.startTime) + selectedEnd = convertStringToLocalDateTime(selectedItem.endTime) } else { - val itemsForToday = getItemsOfDay(date, state) + val itemsForToday = getItems(categoryState, itemState, date) selectedStart = if (itemsForToday.isNotEmpty() && itemsForToday.last().endTime.isNotBlank()) { - parseToLocalDateTime(itemsForToday.last().endTime) + convertStringToLocalDateTime(itemsForToday.last().endTime) } else { dateTime } @@ -331,13 +334,15 @@ fun HomeTabEditItemDialog( val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS") if (selectedItem != null) { - onEvent(ItemEvent.SetId(selectedItem.id)) + itemEvent(ItemEvent.SetId(selectedItem.id)) + itemEvent(ItemEvent.SetCategoryId(selectedItem.categoryId)) } - onEvent(ItemEvent.SetStart(selectedStart.format(formatter))) - onEvent(ItemEvent.SetEnd(selectedEnd.format(formatter))) - onEvent(ItemEvent.SetOngoing(false)) - onEvent(ItemEvent.SetPause(selectedPause)) - onEvent(ItemEvent.SaveItem) + itemEvent(ItemEvent.SetStart(selectedStart.format(formatter))) + itemEvent(ItemEvent.SetEnd(selectedEnd.format(formatter))) + itemEvent(ItemEvent.SetOngoing(false)) + itemEvent(ItemEvent.SetPause(selectedPause)) + + itemEvent(ItemEvent.SaveItem) onClose() }, diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabFab.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabFab.kt index afe25916..61407feb 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabFab.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabFab.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Category import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Stop @@ -33,9 +34,14 @@ import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cloud.pablos.overload.R +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.Helpers.Companion.decideForeground +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState -import cloud.pablos.overload.data.item.startOrStopPause +import cloud.pablos.overload.data.item.fabPress import cloud.pablos.overload.ui.views.TextView import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest @@ -44,12 +50,17 @@ import java.time.LocalDate @RequiresApi(Build.VERSION_CODES.S) @Composable fun HomeTabFab( - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryEvent: (CategoryEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { + val backgroundColor = decideBackground(categoryState) + val foregroundColor = decideForeground(backgroundColor) + val date = LocalDate.now() - val itemsForToday = getItemsOfDay(date, state) + val itemsForToday = getItems(categoryState, itemState, date) val isOngoing = itemsForToday.isNotEmpty() && itemsForToday.last().ongoing val interactionSource = remember { MutableInteractionSource() } @@ -68,8 +79,8 @@ fun HomeTabFab( delay(viewConfiguration.longPressTimeoutMillis) isLongClick = true - onEvent(ItemEvent.SetIsFabOpen(true)) - onEvent(ItemEvent.SetIsDeletingHome(false)) + itemEvent(ItemEvent.SetIsFabOpen(true)) + itemEvent(ItemEvent.SetIsDeletingHome(false)) } is PressInteraction.Release -> { } @@ -84,8 +95,39 @@ fun HomeTabFab( horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(16.dp), ) { - when (state.isFabOpen) { + when (itemState.isFabOpen) { true -> { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier.padding(end = 8.dp), + ) { + TextView( + text = stringResource(id = R.string.switch_category), + modifier = + Modifier + .clip(RoundedCornerShape(12.dp)) + .background(color = MaterialTheme.colorScheme.surfaceContainer) + .padding(horizontal = 8.dp, vertical = 4.dp), + ) + } + + SmallFloatingActionButton( + onClick = { + itemEvent(ItemEvent.SetIsFabOpen(false)) + categoryEvent(CategoryEvent.SetIsSwitchCategoryDialogOpenHome(true)) + }, + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) { + Icon( + imageVector = Icons.Default.Category, + contentDescription = stringResource(id = R.string.switch_category), + ) + } + } + Row( verticalAlignment = Alignment.CenterVertically, ) { @@ -104,11 +146,11 @@ fun HomeTabFab( SmallFloatingActionButton( onClick = { - onEvent(ItemEvent.SetIsFabOpen(false)) + itemEvent(ItemEvent.SetIsFabOpen(false)) manualDialogState.value = true }, - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.primaryContainer, + containerColor = backgroundColor, + contentColor = foregroundColor, ) { Icon( imageVector = Icons.Default.Add, @@ -119,7 +161,7 @@ fun HomeTabFab( FloatingActionButton( onClick = { - onEvent(ItemEvent.SetIsFabOpen(false)) + itemEvent(ItemEvent.SetIsFabOpen(false)) }, containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer, @@ -136,12 +178,12 @@ fun HomeTabFab( FloatingActionButton( onClick = { if (isLongClick.not()) { - startOrStopPause(state, onEvent) + fabPress(categoryState, categoryEvent, itemState, itemEvent) } }, interactionSource = interactionSource, - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = backgroundColor, + contentColor = foregroundColor, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -177,6 +219,6 @@ fun HomeTabFab( } if (manualDialogState.value) { - HomeTabManualDialog(onClose = { manualDialogState.value = false }, state, onEvent) + HomeTabManualDialog(onClose = { manualDialogState.value = false }, categoryState, itemState, itemEvent) } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabItems.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabItems.kt index bbff3d4d..77b6a050 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabItems.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabItems.kt @@ -44,30 +44,33 @@ val homeTabItems = listOf( TabItem( titleResId = dayBeforeYesterdayResId, - screen = { state, onEvent -> + screen = { categoryState, state, itemEvent -> DayView( - state = state, - onEvent = onEvent, + categoryState, + itemState = state, + itemEvent = itemEvent, date = LocalDate.now().minusDays(2), ) }, ), TabItem( titleResId = R.string.yesterday, - screen = { state, onEvent -> + screen = { categoryState, state, itemEvent -> DayView( - state = state, - onEvent = onEvent, + categoryState, + itemState = state, + itemEvent = itemEvent, date = LocalDate.now().minusDays(1), ) }, ), TabItem( titleResId = R.string.today, - screen = { state, onEvent -> + screen = { categoryState, state, itemEvent -> DayView( - state = state, - onEvent = onEvent, + categoryState, + itemState = state, + itemEvent = itemEvent, date = LocalDate.now(), ) }, diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabManualDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabManualDialog.kt index ac4a463d..06f6fda9 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabManualDialog.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabManualDialog.kt @@ -36,10 +36,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import cloud.pablos.overload.R +import cloud.pablos.overload.data.Converters.Companion.convertStringToLocalDateTime +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.Helpers.Companion.decideForeground +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState import cloud.pablos.overload.ui.views.TextView -import cloud.pablos.overload.ui.views.parseToLocalDateTime import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -48,9 +52,13 @@ import java.util.Calendar @Composable fun HomeTabManualDialog( onClose: () -> Unit, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { + val backgroundColor = decideBackground(categoryState) + val foregroundColor = decideForeground(backgroundColor) + val context = LocalContext.current val date = LocalDate.now() @@ -65,11 +73,11 @@ fun HomeTabManualDialog( var selectedEndDateText by remember { mutableStateOf("") } var selectedEndTimeText by remember { mutableStateOf("") } - val itemsForToday = getItemsOfDay(date, state) + val itemsForToday = getItems(categoryState, itemState, date) selectedStart = if (itemsForToday.isNotEmpty() && itemsForToday.last().endTime.isNotBlank()) { - parseToLocalDateTime(itemsForToday.last().endTime) + convertStringToLocalDateTime(itemsForToday.last().endTime) } else { dateTime } @@ -203,26 +211,26 @@ fun HomeTabManualDialog( ) { TextView( text = selectedStartDateText, - color = MaterialTheme.colorScheme.onTertiaryContainer, + color = foregroundColor, modifier = Modifier .clip(RoundedCornerShape(12.dp)) .clickable { startDatePicker.show() } - .background(color = MaterialTheme.colorScheme.tertiaryContainer) + .background(color = backgroundColor) .padding(horizontal = 10.dp, vertical = 6.dp), ) TextView( text = selectedStartTimeText, - color = MaterialTheme.colorScheme.onTertiaryContainer, + color = foregroundColor, modifier = Modifier .clip(RoundedCornerShape(12.dp)) .clickable { startTimePicker.show() } - .background(color = MaterialTheme.colorScheme.tertiaryContainer) + .background(color = backgroundColor) .padding(horizontal = 10.dp, vertical = 6.dp), ) } @@ -236,26 +244,26 @@ fun HomeTabManualDialog( ) { TextView( text = selectedEndDateText, - color = MaterialTheme.colorScheme.onTertiaryContainer, + color = foregroundColor, modifier = Modifier .clip(RoundedCornerShape(12.dp)) .clickable { endDatePicker.show() } - .background(color = MaterialTheme.colorScheme.tertiaryContainer) + .background(color = backgroundColor) .padding(horizontal = 10.dp, vertical = 6.dp), ) TextView( text = selectedEndTimeText, - color = MaterialTheme.colorScheme.onTertiaryContainer, + color = foregroundColor, modifier = Modifier .clip(RoundedCornerShape(12.dp)) .clickable { endTimePicker.show() } - .background(color = MaterialTheme.colorScheme.tertiaryContainer) + .background(color = backgroundColor) .padding(horizontal = 10.dp, vertical = 6.dp), ) } @@ -268,6 +276,18 @@ fun HomeTabManualDialog( verticalAlignment = Alignment.CenterVertically, ) { FilterChip( + colors = + FilterChipDefaults.filterChipColors( + selectedContainerColor = backgroundColor, + labelColor = foregroundColor, + iconColor = foregroundColor, + ), + border = + FilterChipDefaults.filterChipBorder( + enabled = true, + selected = selectedPause, + borderColor = backgroundColor, + ), onClick = { selectedPause = true }, label = { TextView( @@ -287,6 +307,18 @@ fun HomeTabManualDialog( ) FilterChip( + colors = + FilterChipDefaults.filterChipColors( + selectedContainerColor = backgroundColor, + labelColor = foregroundColor, + iconColor = foregroundColor, + ), + border = + FilterChipDefaults.filterChipBorder( + enabled = true, + selected = selectedPause, + borderColor = backgroundColor, + ), onClick = { selectedPause = false }, label = { TextView( @@ -312,18 +344,19 @@ fun HomeTabManualDialog( onClick = { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS") - onEvent(ItemEvent.SetStart(selectedStart.format(formatter))) - onEvent(ItemEvent.SetEnd(selectedEnd.format(formatter))) - onEvent(ItemEvent.SetOngoing(false)) - onEvent(ItemEvent.SetPause(selectedPause)) - onEvent(ItemEvent.SaveItem) + itemEvent(ItemEvent.SetStart(selectedStart.format(formatter))) + itemEvent(ItemEvent.SetEnd(selectedEnd.format(formatter))) + itemEvent(ItemEvent.SetOngoing(false)) + itemEvent(ItemEvent.SetPause(selectedPause)) + itemEvent(ItemEvent.SetCategoryId(categoryState.selectedCategory)) + itemEvent(ItemEvent.SaveItem) onClose() }, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = backgroundColor, + contentColor = foregroundColor, ), ) { TextView(stringResource(id = R.string.save)) diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabProgress.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabProgress.kt index 1d63940b..e323feb2 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabProgress.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabProgress.kt @@ -47,8 +47,9 @@ fun HomeTabProgress( tonalElevation = NavigationBarDefaults.Elevation, color = MaterialTheme.colorScheme.background, shape = RoundedCornerShape(30.dp), - modifier = Modifier - .fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth(), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -84,6 +85,7 @@ fun HomeTabProgress( } } } + class ProgressData( progress: State, color: State, @@ -104,32 +106,35 @@ fun HomeTabProgressPreview() { val transition = updateTransition(targetState = duration, label = "progress") // Progress - val progress = transition.animateFloat( - transitionSpec = { tween(800) }, - label = "progress", - ) { remTime -> - val calculatedProgress = if (remTime < 0) { - 360f - } else { - 360f - ((360f / goal) * (goal - remTime)) - } + val progress = + transition.animateFloat( + transitionSpec = { tween(800) }, + label = "progress", + ) { remTime -> + val calculatedProgress = + if (remTime < 0) { + 360f + } else { + 360f - ((360f / goal) * (goal - remTime)) + } - calculatedProgress.coerceAtMost(360f) - } + calculatedProgress.coerceAtMost(360f) + } // Color - val color = transition.animateColor( - transitionSpec = { - tween(800, easing = LinearEasing) - }, - label = "Color transition", - ) { - if (progress.value < 360f) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary + val color = + transition.animateColor( + transitionSpec = { + tween(800, easing = LinearEasing) + }, + label = "Color transition", + ) { + if (progress.value < 360f) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + } } - } // Text val title = stringResource(id = R.string.pause_left) diff --git a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabTopAppBar.kt b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabTopAppBar.kt index ea9539c1..58950d26 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabTopAppBar.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/tabs/home/HomeTabTopAppBar.kt @@ -1,5 +1,7 @@ package cloud.pablos.overload.ui.tabs.home +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarDefaults @@ -9,11 +11,18 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import cloud.pablos.overload.R +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState +import cloud.pablos.overload.ui.views.ChangeCategoryButton import cloud.pablos.overload.ui.views.TextView +@RequiresApi(Build.VERSION_CODES.S) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeTabTopAppBar() { +fun HomeTabTopAppBar( + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, +) { Surface( tonalElevation = NavigationBarDefaults.Elevation, color = MaterialTheme.colorScheme.background, @@ -30,6 +39,9 @@ fun HomeTabTopAppBar() { containerColor = MaterialTheme.colorScheme.background, titleContentColor = MaterialTheme.colorScheme.onBackground, ), + actions = { + ChangeCategoryButton(categoryState, categoryEvent) + }, ) } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/AdjustEndDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/views/AdjustEndDialog.kt index 26046b8e..0c4e9c1c 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/views/AdjustEndDialog.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/views/AdjustEndDialog.kt @@ -38,10 +38,14 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.core.net.toUri import cloud.pablos.overload.R +import cloud.pablos.overload.data.Converters.Companion.convertStringToLocalDateTime +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.Helpers.Companion.decideForeground +import cloud.pablos.overload.data.Helpers.Companion.getItemsPastDays +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.Item import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState -import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -49,26 +53,24 @@ import java.time.format.DateTimeFormatter @Composable fun AdjustEndDialog( onClose: () -> Unit, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { - val context = LocalContext.current - val learnMoreLink = "https://codeberg.org/pabloscloud/Overload#spread-acorss-days".toUri() + val backgroundColor = decideBackground(categoryState) + val foregroundColor = decideForeground(backgroundColor) - val date = LocalDate.now() + val context = LocalContext.current + val learnMoreLink = "https://github.com/pabloscloud/Overload?tab=readme-ov-file#why-does-the-app-annoy-me-with-a-popup-to-adjust-the-end".toUri() - val itemsNotToday = - state.items.filter { item -> - val startTime = parseToLocalDateTime(item.startTime) - extractDate(startTime) != date - } + val itemsNotToday = getItemsPastDays(categoryState, itemState) val isOngoingNotToday = itemsNotToday.isNotEmpty() && itemsNotToday.any { it.ongoing } val firstOngoingItem = itemsNotToday.find { it.ongoing } var selectedTimeText by remember { mutableStateOf("") } if (isOngoingNotToday && firstOngoingItem != null) { - val startTime = parseToLocalDateTime(firstOngoingItem.startTime) + val startTime = convertStringToLocalDateTime(firstOngoingItem.startTime) var endTime by remember { mutableStateOf(startTime) } val timePicker = @@ -134,7 +136,13 @@ fun AdjustEndDialog( Spacer(modifier = Modifier.height(16.dp)) - DayViewItemOngoing(item = firstOngoingItem, showDate = true, hideEnd = true, state = state) + DayViewItemOngoing( + item = firstOngoingItem, + showDate = true, + hideEnd = true, + categoryState = categoryState, + itemState = itemState, + ) Spacer(modifier = Modifier.height(16.dp)) @@ -162,12 +170,12 @@ fun AdjustEndDialog( confirmButton = { Button( onClick = { - onClose.save(onEvent, firstOngoingItem, endTime) + onClose.save(itemEvent, firstOngoingItem, endTime) }, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = backgroundColor, + contentColor = foregroundColor, ), ) { TextView(stringResource(R.string.save)) @@ -195,18 +203,18 @@ fun AdjustEndDialog( } private fun (() -> Unit).save( - onEvent: (ItemEvent) -> Unit, + itemEvent: (ItemEvent) -> Unit, item: Item, newEnd: LocalDateTime, ) { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS") - onEvent(ItemEvent.SetId(item.id)) - onEvent(ItemEvent.SetStart(item.startTime)) - onEvent(ItemEvent.SetEnd(newEnd.format(formatter))) - onEvent(ItemEvent.SetOngoing(false)) - onEvent(ItemEvent.SetPause(item.pause)) - onEvent(ItemEvent.SaveItem) + itemEvent(ItemEvent.SetId(item.id)) + itemEvent(ItemEvent.SetStart(item.startTime)) + itemEvent(ItemEvent.SetEnd(newEnd.format(formatter))) + itemEvent(ItemEvent.SetOngoing(false)) + itemEvent(ItemEvent.SetPause(item.pause)) + itemEvent(ItemEvent.SaveItem) this() } diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/ChangeCategoryButton.kt b/app/src/main/java/cloud/pablos/overload/ui/views/ChangeCategoryButton.kt new file mode 100644 index 00000000..e2b71815 --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/ui/views/ChangeCategoryButton.kt @@ -0,0 +1,53 @@ +package cloud.pablos.overload.ui.views + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.Helpers.Companion.decideForeground +import cloud.pablos.overload.data.Helpers.Companion.getSelectedCategory +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState + +@RequiresApi(Build.VERSION_CODES.S) +@Composable +fun ChangeCategoryButton( + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, +) { + val categoryDialogState = remember { mutableStateOf(false) } + + val categoriesCount = categoryState.categories.count() + val selectedCategory = getSelectedCategory(categoryState) + + if (categoriesCount > 1 && selectedCategory != null) { + val backgroundColor = decideBackground(categoryState) + val foregroundColor = decideForeground(backgroundColor) + + Button( + onClick = { categoryDialogState.value = true }, + modifier = Modifier.padding(horizontal = 8.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = backgroundColor, + contentColor = foregroundColor, + ), + ) { + TextView(selectedCategory.emoji) + } + if (categoryDialogState.value) { + SwitchCategoryDialog( + categoryState, + categoryEvent, + onClose = { categoryDialogState.value = false }, + ) + } + } +} diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/ChangeYearButton.kt b/app/src/main/java/cloud/pablos/overload/ui/views/ChangeYearButton.kt new file mode 100644 index 00000000..c688ee06 --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/ui/views/ChangeYearButton.kt @@ -0,0 +1,54 @@ +package cloud.pablos.overload.ui.views + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cloud.pablos.overload.data.Converters +import cloud.pablos.overload.data.item.ItemEvent +import cloud.pablos.overload.data.item.ItemState +import cloud.pablos.overload.ui.tabs.calendar.CalendarTabYearDialog +import java.time.LocalDate + +@RequiresApi(Build.VERSION_CODES.S) +@Composable +fun ChangeYearButton( + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, +) { + val yearDialogState = remember { mutableStateOf(false) } + + val firstYear = + if (itemState.items.isEmpty()) { + LocalDate.now().year + } else { + itemState.items.minByOrNull { it.startTime }?.let { + Converters.convertStringToLocalDateTime( + it.startTime, + ).year + } ?: LocalDate.now().year + } + + val yearsCount = LocalDate.now().year - firstYear + + if (yearsCount > 0) { + Button( + onClick = { yearDialogState.value = true }, + modifier = Modifier.padding(horizontal = 8.dp), + ) { + TextView(itemState.selectedYearCalendar.toString()) + } + if (yearDialogState.value) { + CalendarTabYearDialog( + firstYear = firstYear, + itemEvent = itemEvent, + onClose = { yearDialogState.value = false }, + ) + } + } +} diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/DayScreenDayView.kt b/app/src/main/java/cloud/pablos/overload/ui/views/DayScreenDayView.kt index cb481559..39e98941 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/views/DayScreenDayView.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/views/DayScreenDayView.kt @@ -15,25 +15,23 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import cloud.pablos.overload.R +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.Helpers.Companion.getSelectedCategory +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.Item import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState -import cloud.pablos.overload.ui.tabs.configurations.OlSharedPreferences import cloud.pablos.overload.ui.tabs.home.HomeTabDeletePauseDialog import cloud.pablos.overload.ui.tabs.home.HomeTabEditItemDialog -import cloud.pablos.overload.ui.tabs.home.getItemsOfDay import java.time.LocalDate @SuppressLint("UnusedTransitionTargetStateParameter") @@ -43,27 +41,26 @@ import java.time.LocalDate fun DayScreenDayView( daysCount: Int, page: Int, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { val date = LocalDate.now() .minusDays((daysCount - page - 1).toLong()) - val items = getItemsOfDay(date, state) - + val items = getItems(categoryState, itemState, date) val itemsDesc = items.sortedByDescending { it.startTime } val deletePauseDialogState = remember { mutableStateOf(false) } val editItemDialogState = remember { mutableStateOf(false) } - val context = LocalContext.current - val sharedPreferences = remember { OlSharedPreferences(context) } + val selectedCategory = getSelectedCategory(categoryState) - val goalWork by remember { mutableIntStateOf(sharedPreferences.getWorkGoal()) } - val goalPause by remember { mutableIntStateOf(sharedPreferences.getPauseGoal()) } + if (itemsDesc.isNotEmpty() && selectedCategory != null) { + val goal1 = selectedCategory.goal1 + val goal2 = selectedCategory.goal2 - if (itemsDesc.isNotEmpty()) { LazyColumn( modifier = Modifier.fillMaxSize(), ) { @@ -75,20 +72,26 @@ fun DayScreenDayView( .padding(top = 10.dp, start = 10.dp, end = 10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - if (goalWork > 0) { + if (goal1 > 0) { Box( modifier = Modifier.weight(1f), ) { - DayViewProgress(goal = goalWork, items = items, isPause = false) + DayViewProgress( + category = selectedCategory, + goal = goal1, + items = items, + isPause = false, + ) } } - if (goalPause > 0) { + if (goal2 > 0) { Box( modifier = Modifier.weight(1f), ) { DayViewProgress( - goal = goalPause, + category = selectedCategory, + goal = goal2, items = items, date = date, isPause = true, @@ -114,10 +117,10 @@ fun DayScreenDayView( deletePauseDialogState.value = true }, onClick = { - if (state.isDeletingHome) { + if (itemState.isDeletingHome) { deletePauseDialogState.value = true } else { - onEvent(ItemEvent.SetSelectedItemsHome(emptyList())) + itemEvent(ItemEvent.SetSelectedItemsHome(emptyList())) editItemDialogState.value = true } }, @@ -131,9 +134,11 @@ fun DayScreenDayView( endTime = LocalDate.now().toString(), ongoing = true, pause = true, + categoryId = categoryState.selectedCategory, ), isSelected = false, - state = state, + categoryState = categoryState, + itemState = itemState, ) } } @@ -144,7 +149,7 @@ fun DayScreenDayView( val isLastItem = index == itemSize - 1 val item = itemsDesc[index] - val isSelected = state.selectedItemsHome.contains(item) + val isSelected = itemState.selectedItemsHome.contains(item) Box( modifier = Modifier @@ -156,22 +161,22 @@ fun DayScreenDayView( ) .combinedClickable( onLongClick = { - onEvent(ItemEvent.SetIsDeletingHome(true)) - onEvent(ItemEvent.SetSelectedItemsHome(listOf(item))) - onEvent(ItemEvent.SetIsFabOpen(false)) + itemEvent(ItemEvent.SetIsDeletingHome(true)) + itemEvent(ItemEvent.SetSelectedItemsHome(listOf(item))) + itemEvent(ItemEvent.SetIsFabOpen(false)) }, onClick = { - if (state.isDeletingHome) { + if (itemState.isDeletingHome) { when (isSelected) { true -> - onEvent( - ItemEvent.SetSelectedItemsHome(state.selectedItemsHome.filterNot { it == item }), + itemEvent( + ItemEvent.SetSelectedItemsHome(itemState.selectedItemsHome.filterNot { it == item }), ) else -> - onEvent( + itemEvent( ItemEvent.SetSelectedItemsHome( - state.selectedItemsHome + + itemState.selectedItemsHome + listOf( item, ), @@ -179,15 +184,15 @@ fun DayScreenDayView( ) } } else { - onEvent(ItemEvent.SetSelectedItemsHome(listOf(item))) + itemEvent(ItemEvent.SetSelectedItemsHome(listOf(item))) editItemDialogState.value = true } }, ), ) { when (item.ongoing.not() && item.endTime.isNotBlank()) { - true -> DayViewItemNotOngoing(item, isSelected = isSelected, state) - else -> DayViewItemOngoing(item, isSelected = isSelected, state = state) + true -> DayViewItemNotOngoing(item, categoryState, itemState, isSelected) + else -> DayViewItemOngoing(item, categoryState, itemState, isSelected) } } } @@ -224,11 +229,12 @@ fun DayScreenDayView( if (editItemDialogState.value) { HomeTabEditItemDialog( onClose = { - onEvent(ItemEvent.SetSelectedItemsHome(emptyList())) + itemEvent(ItemEvent.SetSelectedItemsHome(emptyList())) editItemDialogState.value = false }, - state, - onEvent, + categoryState, + itemState, + itemEvent, ) } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/DayView.kt b/app/src/main/java/cloud/pablos/overload/ui/views/DayView.kt index 832ac2ca..411b278c 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/views/DayView.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/views/DayView.kt @@ -15,55 +15,49 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import cloud.pablos.overload.R +import cloud.pablos.overload.data.Helpers.Companion.getItems +import cloud.pablos.overload.data.Helpers.Companion.getSelectedCategory +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.Item import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState -import cloud.pablos.overload.ui.tabs.configurations.OlSharedPreferences import cloud.pablos.overload.ui.tabs.home.HomeTabDeletePauseDialog import cloud.pablos.overload.ui.tabs.home.HomeTabEditItemDialog -import cloud.pablos.overload.ui.tabs.home.getItemsOfDay import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import java.time.format.DateTimeFormatterBuilder -import java.time.format.DateTimeParseException -import java.time.temporal.ChronoField @SuppressLint("UnusedTransitionTargetStateParameter") @OptIn(ExperimentalFoundationApi::class) @RequiresApi(Build.VERSION_CODES.S) @Composable fun DayView( - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, date: LocalDate, ) { - val items = getItemsOfDay(date, state) + val selectedCategory = getSelectedCategory(categoryState) + val items = getItems(categoryState, itemState, date) val itemsDesc = items.sortedByDescending { it.startTime } val deletePauseDialogState = remember { mutableStateOf(false) } val editItemDialogState = remember { mutableStateOf(false) } - val context = LocalContext.current - val sharedPreferences = remember { OlSharedPreferences(context) } + if (itemsDesc.isNotEmpty() && selectedCategory != null) { + val goal1 = selectedCategory.goal1 + val goal2 = selectedCategory.goal2 - val goalWork by remember { mutableIntStateOf(sharedPreferences.getWorkGoal()) } - val goalPause by remember { mutableIntStateOf(sharedPreferences.getPauseGoal()) } - - if (itemsDesc.isNotEmpty()) { LazyColumn( modifier = Modifier.fillMaxSize(), ) { @@ -75,20 +69,21 @@ fun DayView( .padding(top = 10.dp, start = 10.dp, end = 10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - if (goalWork > 0) { + if (goal1 > 0) { Box( modifier = Modifier.weight(1f), ) { - DayViewProgress(goal = goalWork, items = items, isPause = false) + DayViewProgress(category = selectedCategory, goal = goal1, items = items, isPause = false) } } - if (goalPause > 0) { + if (goal2 > 0) { Box( modifier = Modifier.weight(1f), ) { DayViewProgress( - goal = goalPause, + category = selectedCategory, + goal = goal2, items = items, date = date, isPause = true, @@ -114,10 +109,10 @@ fun DayView( deletePauseDialogState.value = true }, onClick = { - if (state.isDeletingHome) { + if (itemState.isDeletingHome) { deletePauseDialogState.value = true } else { - onEvent(ItemEvent.SetSelectedItemsHome(emptyList())) + itemEvent(ItemEvent.SetSelectedItemsHome(emptyList())) editItemDialogState.value = true } }, @@ -131,9 +126,11 @@ fun DayView( endTime = LocalDate.now().toString(), ongoing = true, pause = true, + categoryId = categoryState.selectedCategory, ), isSelected = false, - state = state, + categoryState = categoryState, + itemState = itemState, ) } } @@ -144,7 +141,7 @@ fun DayView( val isLastItem = index == itemSize - 1 val item = itemsDesc[index] - val isSelected = state.selectedItemsHome.contains(item) + val isSelected = itemState.selectedItemsHome.contains(item) Box( modifier = Modifier @@ -156,22 +153,22 @@ fun DayView( ) .combinedClickable( onLongClick = { - onEvent(ItemEvent.SetIsDeletingHome(true)) - onEvent(ItemEvent.SetSelectedItemsHome(listOf(item))) - onEvent(ItemEvent.SetIsFabOpen(false)) + itemEvent(ItemEvent.SetIsDeletingHome(true)) + itemEvent(ItemEvent.SetSelectedItemsHome(listOf(item))) + itemEvent(ItemEvent.SetIsFabOpen(false)) }, onClick = { - if (state.isDeletingHome) { + if (itemState.isDeletingHome) { when (isSelected) { true -> - onEvent( - ItemEvent.SetSelectedItemsHome(state.selectedItemsHome.filterNot { it == item }), + itemEvent( + ItemEvent.SetSelectedItemsHome(itemState.selectedItemsHome.filterNot { it == item }), ) else -> - onEvent( + itemEvent( ItemEvent.SetSelectedItemsHome( - state.selectedItemsHome + + itemState.selectedItemsHome + listOf( item, ), @@ -179,15 +176,15 @@ fun DayView( ) } } else { - onEvent(ItemEvent.SetSelectedItemsHome(listOf(item))) + itemEvent(ItemEvent.SetSelectedItemsHome(listOf(item))) editItemDialogState.value = true } }, ), ) { when (item.ongoing.not() && item.endTime.isNotBlank()) { - true -> DayViewItemNotOngoing(item, isSelected = isSelected, state) - else -> DayViewItemOngoing(item, isSelected = isSelected, state = state) + true -> DayViewItemNotOngoing(item, categoryState, itemState, isSelected) + else -> DayViewItemOngoing(item, categoryState, itemState, isSelected) } } } @@ -224,31 +221,16 @@ fun DayView( if (editItemDialogState.value) { HomeTabEditItemDialog( onClose = { - onEvent(ItemEvent.SetSelectedItemsHome(emptyList())) + itemEvent(ItemEvent.SetSelectedItemsHome(emptyList())) editItemDialogState.value = false }, - state, - onEvent, + categoryState, + itemState, + itemEvent, ) } } -fun parseToLocalDateTime(dateTimeString: String): LocalDateTime { - val formatter = - DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd'T'HH:mm:ss") - .optionalStart() - .appendFraction(ChronoField.MICRO_OF_SECOND, 0, 6, true) - .optionalEnd() - .toFormatter() - - return try { - LocalDateTime.parse(dateTimeString, formatter) - } catch (e: DateTimeParseException) { - return LocalDateTime.now() - } -} - fun getLocalDate(selectedDay: String): LocalDate { val date: LocalDate = if (selectedDay.isNotBlank()) { diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/DayViewItemNotOngoing.kt b/app/src/main/java/cloud/pablos/overload/ui/views/DayViewItemNotOngoing.kt index fbfdcd51..f1ef13ee 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/views/DayViewItemNotOngoing.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/views/DayViewItemNotOngoing.kt @@ -29,6 +29,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import cloud.pablos.overload.R +import cloud.pablos.overload.data.Converters.Companion.convertStringToLocalDateTime +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.Helpers.Companion.decideForeground +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.Item import cloud.pablos.overload.data.item.ItemState import java.time.LocalDateTime @@ -38,9 +42,13 @@ import java.time.format.DateTimeFormatter @Composable fun DayViewItemNotOngoing( item: Item, + categoryState: CategoryState, + itemState: ItemState, isSelected: Boolean, - state: ItemState, ) { + val backgroundColorCategory = decideBackground(categoryState) + val foregroundColorCategory = decideForeground(backgroundColorCategory) + var backgroundColor: Color var foregroundColor: Color @@ -48,11 +56,11 @@ fun DayViewItemNotOngoing( val parsedEndTime: LocalDateTime item.let { - parsedStartTime = parseToLocalDateTime(it.startTime) - parsedEndTime = parseToLocalDateTime(it.endTime) + parsedStartTime = convertStringToLocalDateTime(it.startTime) + parsedEndTime = convertStringToLocalDateTime(it.endTime) when (isSelected) { true -> { - when (state.isDeletingHome) { + when (itemState.isDeletingHome) { true -> { backgroundColor = MaterialTheme.colorScheme.errorContainer foregroundColor = MaterialTheme.colorScheme.onErrorContainer @@ -73,8 +81,8 @@ fun DayViewItemNotOngoing( } false -> { - backgroundColor = MaterialTheme.colorScheme.onSurfaceVariant - foregroundColor = MaterialTheme.colorScheme.surfaceVariant + backgroundColor = backgroundColorCategory + foregroundColor = foregroundColorCategory } } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/DayViewItemOngoing.kt b/app/src/main/java/cloud/pablos/overload/ui/views/DayViewItemOngoing.kt index 0d3bad4a..b8eb1ccd 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/views/DayViewItemOngoing.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/views/DayViewItemOngoing.kt @@ -38,6 +38,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import cloud.pablos.overload.R +import cloud.pablos.overload.data.Converters.Companion.convertStringToLocalDateTime +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.Helpers.Companion.decideForeground +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.Item import cloud.pablos.overload.data.item.ItemState import kotlinx.coroutines.delay @@ -50,18 +54,22 @@ import java.time.format.DateTimeFormatter @Composable fun DayViewItemOngoing( item: Item, + categoryState: CategoryState, + itemState: ItemState, isSelected: Boolean = false, showDate: Boolean = false, hideEnd: Boolean = false, - state: ItemState, ) { + val backgroundColorCategory = decideBackground(categoryState) + val foregroundColorCategory = decideForeground(backgroundColorCategory) + var backgroundColor: Color var foregroundColor: Color var blink by remember { mutableStateOf(true) } LaunchedEffect(blink) { while (true) { - delay(500) // Blink every 500ms + delay(500) blink = blink.not() } } @@ -70,12 +78,12 @@ fun DayViewItemOngoing( val parsedEndTime: LocalDateTime item.let { - parsedStartTime = parseToLocalDateTime(it.startTime) + parsedStartTime = convertStringToLocalDateTime(it.startTime) parsedEndTime = LocalDateTime.now() when (isSelected) { true -> { - when (state.isDeletingHome) { + when (itemState.isDeletingHome) { true -> { backgroundColor = MaterialTheme.colorScheme.errorContainer foregroundColor = MaterialTheme.colorScheme.onErrorContainer @@ -96,8 +104,8 @@ fun DayViewItemOngoing( } false -> { - backgroundColor = MaterialTheme.colorScheme.onSurfaceVariant - foregroundColor = MaterialTheme.colorScheme.surfaceVariant + backgroundColor = backgroundColorCategory + foregroundColor = foregroundColorCategory } } } diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/DayViewProgress.kt b/app/src/main/java/cloud/pablos/overload/ui/views/DayViewProgress.kt index 1dc68664..13f44bcc 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/views/DayViewProgress.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/views/DayViewProgress.kt @@ -12,6 +12,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import cloud.pablos.overload.R +import cloud.pablos.overload.data.Converters.Companion.convertLongToColor +import cloud.pablos.overload.data.Converters.Companion.convertStringToLocalDateTime +import cloud.pablos.overload.data.category.Category import cloud.pablos.overload.data.item.Item import cloud.pablos.overload.ui.tabs.home.HomeTabProgress import cloud.pablos.overload.ui.tabs.home.ProgressData @@ -23,6 +26,7 @@ import java.time.LocalDateTime @RequiresApi(Build.VERSION_CODES.S) @Composable fun DayViewProgress( + category: Category, items: List, goal: Int, date: LocalDate = LocalDate.now(), @@ -47,12 +51,12 @@ fun DayViewProgress( // Count duration itemsFiltered.forEach { - val parsedStartTime = parseToLocalDateTime(it.startTime) + val parsedStartTime = convertStringToLocalDateTime(it.startTime) val parsedEndTime = if (it.ongoing) { LocalDateTime.now() } else { - parseToLocalDateTime(it.endTime) + convertStringToLocalDateTime(it.endTime) } count += Duration.between(parsedStartTime, parsedEndTime).toMillis() @@ -64,7 +68,7 @@ fun DayViewProgress( items.last().pause.not() && date == LocalDate.now() ) { - val parsedStartTime = parseToLocalDateTime(items.last().endTime) + val parsedStartTime = convertStringToLocalDateTime(items.last().endTime) val parsedEndTime = LocalDateTime.now() count += Duration.between(parsedStartTime, parsedEndTime).toMillis() @@ -101,7 +105,7 @@ fun DayViewProgress( if (progress.value < 360f) { MaterialTheme.colorScheme.error } else { - MaterialTheme.colorScheme.primary + convertLongToColor(category.color) } } @@ -113,7 +117,7 @@ fun DayViewProgress( } false -> { - stringResource(id = R.string.work_left) + "Time left" } } val subtitle = getDurationString(Duration.ofMillis(goal - duration)) diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/DeleteTopAppBar.kt b/app/src/main/java/cloud/pablos/overload/ui/views/DeleteTopAppBar.kt index 74773332..1d868aef 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/views/DeleteTopAppBar.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/views/DeleteTopAppBar.kt @@ -20,11 +20,11 @@ import cloud.pablos.overload.data.item.ItemState @OptIn(ExperimentalMaterial3Api::class) @Composable fun DeleteTopAppBar( - state: ItemState, - onEvent: (ItemEvent) -> Unit, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { BackHandler { - onEvent(ItemEvent.SetIsDeletingHome(false)) + itemEvent(ItemEvent.SetIsDeletingHome(false)) } Surface( @@ -34,7 +34,7 @@ fun DeleteTopAppBar( TopAppBar( title = { TextView( - text = state.selectedItemsHome.size.toString() + " " + stringResource(id = R.string.itemCount_selected), + text = itemState.selectedItemsHome.size.toString() + " " + stringResource(id = R.string.itemCount_selected), fontSize = MaterialTheme.typography.titleLarge.fontSize, ) }, @@ -45,7 +45,7 @@ fun DeleteTopAppBar( ), actions = { IconButton(onClick = { - onEvent(ItemEvent.SetIsDeletingHome(false)) + itemEvent(ItemEvent.SetIsDeletingHome(false)) }) { Icon( imageVector = Icons.Filled.Close, diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/ForgotToStopDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/views/ForgotToStopDialog.kt index ad8746d5..04c0a2b9 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/views/ForgotToStopDialog.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/views/ForgotToStopDialog.kt @@ -27,15 +27,22 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.core.net.toUri import cloud.pablos.overload.R +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.Helpers.Companion.decideForeground +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent @Composable fun ForgotToStopDialog( onClose: () -> Unit, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + itemEvent: (ItemEvent) -> Unit, ) { + val backgroundColor = decideBackground(categoryState) + val foregroundColor = decideForeground(backgroundColor) + val context = LocalContext.current - val learnMoreLink = "https://codeberg.org/pabloscloud/Overload#spread-acorss-days".toUri() + val learnMoreLink = "https://github.com/pabloscloud/Overload?tab=readme-ov-file#why-does-the-app-annoy-me-with-a-popup-to-adjust-the-end".toUri() AlertDialog( onDismissRequest = onClose, @@ -68,7 +75,11 @@ fun ForgotToStopDialog( val openLinkStr = stringResource(id = R.string.open_link_with) ClickableText( text = AnnotatedString(stringResource(id = R.string.learn_more)), - style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.primary, textAlign = TextAlign.Center), + style = + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, + ), onClick = { val intent = Intent(Intent.ACTION_VIEW, learnMoreLink) val chooserIntent = Intent.createChooser(intent, openLinkStr) @@ -81,16 +92,18 @@ fun ForgotToStopDialog( confirmButton = { Button( onClick = { - onEvent(ItemEvent.SetSpreadAcrossDaysDialogShown(true)) + itemEvent(ItemEvent.SetSpreadAcrossDaysDialogShown(true)) onClose() }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), ) { TextView(stringResource(R.string.spread_across_days)) } @@ -98,13 +111,14 @@ fun ForgotToStopDialog( dismissButton = { Button( onClick = { - onEvent(ItemEvent.SetAdjustEndDialogShown(true)) + itemEvent(ItemEvent.SetAdjustEndDialogShown(true)) onClose() }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - ), + colors = + ButtonDefaults.buttonColors( + containerColor = backgroundColor, + contentColor = foregroundColor, + ), modifier = Modifier.fillMaxWidth(), ) { TextView(stringResource(id = R.string.adjust)) diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/SpreadAcrossDaysDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/views/SpreadAcrossDaysDialog.kt index 153ca6c8..f7886eed 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/views/SpreadAcrossDaysDialog.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/views/SpreadAcrossDaysDialog.kt @@ -29,6 +29,10 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.core.net.toUri import cloud.pablos.overload.R +import cloud.pablos.overload.data.Converters.Companion.convertStringToLocalDateTime +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.Helpers.Companion.decideForeground +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.Item import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.data.item.ItemState @@ -41,17 +45,21 @@ import java.time.format.DateTimeFormatter @Composable fun SpreadAcrossDaysDialog( onClose: () -> Unit, - state: ItemState, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + itemState: ItemState, + itemEvent: (ItemEvent) -> Unit, ) { + val backgroundColor = decideBackground(categoryState) + val foregroundColor = decideForeground(backgroundColor) + val context = LocalContext.current - val learnMoreLink = "https://codeberg.org/pabloscloud/Overload#spread-acorss-days".toUri() + val learnMoreLink = "https://github.com/pabloscloud/Overload?tab=readme-ov-file#why-does-the-app-annoy-me-with-a-popup-to-adjust-the-end".toUri() val date = LocalDate.now() val itemsNotToday = - state.items.filter { item -> - val startTime = parseToLocalDateTime(item.startTime) + itemState.items.filter { item -> + val startTime = convertStringToLocalDateTime(item.startTime) extractDate(startTime) != date } val isOngoingNotToday = itemsNotToday.isNotEmpty() && itemsNotToday.any { it.ongoing } @@ -105,7 +113,13 @@ fun SpreadAcrossDaysDialog( Spacer(modifier = Modifier.height(16.dp)) - DayViewItemOngoing(item = firstOngoingItem, showDate = true, hideEnd = true, state = state) + DayViewItemOngoing( + item = firstOngoingItem, + showDate = true, + hideEnd = true, + categoryState = categoryState, + itemState = itemState, + ) } }, confirmButton = { @@ -115,8 +129,8 @@ fun SpreadAcrossDaysDialog( }, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + containerColor = backgroundColor, + contentColor = foregroundColor, ), ) { TextView(stringResource(R.string.no)) @@ -125,12 +139,12 @@ fun SpreadAcrossDaysDialog( dismissButton = { Button( onClick = { - onClose.save(onEvent, firstOngoingItem) + onClose.save(itemEvent, firstOngoingItem) }, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, ), ) { TextView(stringResource(R.string.yes)) @@ -144,12 +158,12 @@ fun SpreadAcrossDaysDialog( } private fun (() -> Unit).save( - onEvent: (ItemEvent) -> Unit, + itemEvent: (ItemEvent) -> Unit, item: Item, ) { val currentDate = LocalDate.now() - val startTime = parseToLocalDateTime(item.startTime) + val startTime = convertStringToLocalDateTime(item.startTime) val startDate = startTime.toLocalDate() var dateIterator = startTime.toLocalDate() @@ -158,7 +172,7 @@ private fun (() -> Unit).save( val newStartTime = if (dateIterator == startDate) startTime else LocalDateTime.of(dateIterator, LocalTime.MIDNIGHT) val newEndTime = if (dateIterator == currentDate) { - parseToLocalDateTime( + convertStringToLocalDateTime( LocalDateTime.now().toString(), ) } else { @@ -166,13 +180,13 @@ private fun (() -> Unit).save( } if (dateIterator == startDate) { - onEvent(ItemEvent.SetId(item.id)) + itemEvent(ItemEvent.SetId(item.id)) } - onEvent(ItemEvent.SetStart(newStartTime.format(formatter))) - onEvent(ItemEvent.SetEnd(newEndTime.format(formatter))) - onEvent(ItemEvent.SetOngoing(false)) - onEvent(ItemEvent.SetPause(item.pause)) - onEvent(ItemEvent.SaveItem) + itemEvent(ItemEvent.SetStart(newStartTime.format(formatter))) + itemEvent(ItemEvent.SetEnd(newEndTime.format(formatter))) + itemEvent(ItemEvent.SetOngoing(false)) + itemEvent(ItemEvent.SetPause(item.pause)) + itemEvent(ItemEvent.SaveItem) dateIterator = dateIterator.plusDays(1) } diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/SwitchCategoryDialog.kt b/app/src/main/java/cloud/pablos/overload/ui/views/SwitchCategoryDialog.kt new file mode 100644 index 00000000..5616c1ba --- /dev/null +++ b/app/src/main/java/cloud/pablos/overload/ui/views/SwitchCategoryDialog.kt @@ -0,0 +1,112 @@ +package cloud.pablos.overload.ui.views + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Category +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.Surface +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 androidx.compose.ui.window.Dialog +import cloud.pablos.overload.R +import cloud.pablos.overload.data.category.Category +import cloud.pablos.overload.data.category.CategoryEvent +import cloud.pablos.overload.data.category.CategoryState + +@Composable +fun SwitchCategoryDialog( + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + onClose: () -> Unit, +) { + Dialog( + onDismissRequest = onClose, + content = { + Surface( + shape = MaterialTheme.shapes.large, + tonalElevation = NavigationBarDefaults.Elevation, + color = MaterialTheme.colorScheme.background, + modifier = Modifier.fillMaxWidth(), + ) { + CategoryDialogContent(categoryState, categoryEvent, onClose) + } + }, + ) +} + +@Composable +private fun CategoryDialogContent( + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + onClose: () -> Unit, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(24.dp), + ) { + Icon( + imageVector = Icons.Rounded.Category, + contentDescription = stringResource(id = R.string.select_category), + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(16.dp), + ) + + TextView( + text = stringResource(id = R.string.select_category), + fontSize = MaterialTheme.typography.titleLarge.fontSize, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), + ) + + CategoryListContent(categoryState, categoryEvent, onClose) + } +} + +@Composable +private fun CategoryListContent( + categoryState: CategoryState, + categoryEvent: (CategoryEvent) -> Unit, + onClose: () -> Unit, +) { + LazyColumn { + itemsIndexed(categoryState.categories) { index, category -> + CategoryRow(category, categoryEvent, onClose) + if (index < categoryState.categories.count() - 1) { + HorizontalDivider() + } + } + } +} + +@Composable +private fun CategoryRow( + category: Category, + categoryEvent: (CategoryEvent) -> Unit, + onClose: () -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { + categoryEvent(CategoryEvent.SetSelectedCategory(category.id)) + onClose() + } + .padding(16.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextView(text = category.emoji + " " + category.name) + } +} diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/TextView.kt b/app/src/main/java/cloud/pablos/overload/ui/views/TextView.kt index d9be039f..d0d60b53 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/views/TextView.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/views/TextView.kt @@ -15,7 +15,7 @@ fun TextView( text: String, modifier: Modifier = Modifier, fontSize: TextUnit = TextUnit.Unspecified, - fontWeight: FontWeight = FontWeight.Normal, + fontWeight: FontWeight? = null, color: Color = Color.Unspecified, align: TextAlign = TextAlign.Left, maxLines: Int = 1, diff --git a/app/src/main/java/cloud/pablos/overload/ui/views/YearView.kt b/app/src/main/java/cloud/pablos/overload/ui/views/YearView.kt index 7419d3ca..d77e5206 100644 --- a/app/src/main/java/cloud/pablos/overload/ui/views/YearView.kt +++ b/app/src/main/java/cloud/pablos/overload/ui/views/YearView.kt @@ -27,6 +27,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import cloud.pablos.overload.data.Helpers.Companion.decideBackground +import cloud.pablos.overload.data.Helpers.Companion.decideForeground +import cloud.pablos.overload.data.category.CategoryState import cloud.pablos.overload.data.item.ItemEvent import cloud.pablos.overload.ui.tabs.home.getFormattedDate import java.time.LocalDate @@ -39,7 +42,8 @@ import java.util.Locale fun YearView( date: LocalDate, year: Int, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + itemEvent: (ItemEvent) -> Unit, bottomPadding: Dp = 0.dp, onNavigate: () -> Unit = {}, highlightSelectedDay: Boolean = false, @@ -80,7 +84,7 @@ fun YearView( if (isLastWeekInLastMonth) bottomPadding else 0.dp, ), ) { - WeekRow(firstDayOfMonth, weekOfMonth, date, highlightSelectedDay, onEvent, onNavigate) + WeekRow(month, firstDayOfMonth, weekOfMonth, date, highlightSelectedDay, categoryState, itemEvent, onNavigate) } } } @@ -103,11 +107,13 @@ fun MonthNameHeader(month: Month) { @RequiresApi(Build.VERSION_CODES.S) @Composable fun WeekRow( + month: Month, firstDayOfMonth: LocalDate, weekOfMonth: Int, date: LocalDate, highlightSelectedDay: Boolean = false, - onEvent: (ItemEvent) -> Unit, + categoryState: CategoryState, + itemEvent: (ItemEvent) -> Unit, onNavigate: () -> Unit, ) { var startOfWeek = firstDayOfMonth.plusWeeks(weekOfMonth.toLong()) @@ -141,25 +147,29 @@ fun WeekRow( val today = LocalDate.now() while (iterationDate < endDayOfWeek) { - val (backgroundColor, borderColor) = - getColorOfDay( + if (iterationDate.month == month) { + val colors = + getColorOfDay( + categoryState = categoryState, + date = iterationDate, + firstDayOfMonth = firstDayOfMonth, + selected = date == iterationDate, + highlightSelectedDay = highlightSelectedDay, + ) + val number = iterationDate.dayOfMonth.toString() + val clickable = iterationDate <= today + + DayCell( date = iterationDate, - firstDayOfMonth = firstDayOfMonth, - selected = date == iterationDate, - highlightSelectedDay = highlightSelectedDay, + itemEvent = itemEvent, + colors = colors, + number = number, + clickable = clickable, + onNavigate = onNavigate, ) - val number = iterationDate.dayOfMonth.toString() - val clickable = iterationDate <= today - - DayCell( - date = iterationDate, - onEvent = onEvent, - backgroundColor = backgroundColor, - borderColor = borderColor, - number = number, - clickable = clickable, - onNavigate = onNavigate, - ) + } else { + EmptyDayCell() + } iterationDate = iterationDate.plusDays(1) } @@ -171,9 +181,8 @@ fun WeekRow( @Composable fun DayCell( date: LocalDate, - onEvent: (ItemEvent) -> Unit, - backgroundColor: Color, - borderColor: Color, + itemEvent: (ItemEvent) -> Unit, + colors: DayCellColors, number: String, clickable: Boolean, onNavigate: () -> Unit, @@ -183,12 +192,12 @@ fun DayCell( Modifier .padding() .requiredSize(36.dp) - .background(backgroundColor, shape = CircleShape) + .background(colors.background, shape = CircleShape) .combinedClickable( enabled = clickable, onClick = { - onEvent(ItemEvent.SetSelectedDayCalendar(getFormattedDate(date))) - onEvent(ItemEvent.SetIsSelectedHome(true)) + itemEvent(ItemEvent.SetSelectedDayCalendar(getFormattedDate(date))) + itemEvent(ItemEvent.SetIsSelectedHome(true)) onNavigate() }, indication = @@ -198,12 +207,13 @@ fun DayCell( interactionSource = remember { MutableInteractionSource() }, ) .clip(CircleShape) - .border(2.dp, borderColor, CircleShape), + .border(3.dp, colors.borderColor, CircleShape), contentAlignment = Alignment.Center, ) { TextView( text = number, fontSize = 14.sp, + color = colors.foreground, ) } } @@ -217,47 +227,37 @@ fun EmptyDayCell() { .requiredSize(36.dp) .background(Color.Transparent, shape = CircleShape) .clip(CircleShape) - .border(2.dp, Color.Transparent, CircleShape), + .border(3.dp, Color.Transparent, CircleShape), ) } +data class DayCellColors(val foreground: Color, val background: Color, val borderColor: Color) + @Composable fun getColorOfDay( + categoryState: CategoryState, date: LocalDate, firstDayOfMonth: LocalDate, selected: Boolean, highlightSelectedDay: Boolean = false, -): Pair { +): DayCellColors { val month = firstDayOfMonth.month val today = LocalDate.now() - val backgroundColor = - if (date <= today && date.month == month) { - MaterialTheme.colorScheme.surfaceVariant - } else { - Color.Transparent - } + var backgroundColor = Color.Transparent + var foregroundColor = Color.Unspecified + var borderColor = Color.Transparent - val borderColor = - when (highlightSelectedDay) { - true -> { - if (selected) { - MaterialTheme.colorScheme.primary - } else { - Color.Transparent - } - } + if (selected && highlightSelectedDay) { + backgroundColor = decideBackground(categoryState) + foregroundColor = decideForeground(backgroundColor) + } else if (date <= today && date.month == month) { + backgroundColor = MaterialTheme.colorScheme.surfaceVariant + } - false -> { - if ( - date == LocalDate.now() - ) { - MaterialTheme.colorScheme.primary - } else { - Color.Transparent - } - } - } + if (date == LocalDate.now()) { + borderColor = decideBackground(categoryState) + } - return Pair(backgroundColor, borderColor) + return DayCellColors(foregroundColor, backgroundColor, borderColor) } diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index ca3826a4..00000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to app/src/main/res/mipmap-anydpi/ic_launcher.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to app/src/main/res/mipmap-anydpi/ic_launcher_round.xml diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 16989ba5..ce9c1909 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -50,6 +50,7 @@ Kalender Konfigurationen + Kategorien Ziele Arbeit Setze für die Arbeit ein Ziel diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5a6ca47d..b412234b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,6 +50,7 @@ Calendar Configurations + Categories Goals Work Set a goal for work @@ -61,8 +62,6 @@ Generating the backup failed Import Import from a backup - Import Backup - To import data into the app, please use the Sharesheet from other apps. This allows the app to securely access the shared data and import it into your database. Backup successfully imported Importing the backup failed analytics @@ -101,18 +100,17 @@ Frequency - How often does this crash occur?\n Thanks for patience! Save - Set Work Goal hours minutes Cancel Set Pause Goal - Work left Pause left arrow forward from start to end Select Year + Select Category Translate Want to translate the app? Thanks! Adjust @@ -131,4 +129,7 @@ Thanks for patience! Pause from %s to %s Ongoing entry since %s Ongoing pause since %s + add category + Switch Category + Are you sure you want to delete %s? diff --git a/fastlane/Appfile b/fastlane/Appfile deleted file mode 100644 index e169491c..00000000 --- a/fastlane/Appfile +++ /dev/null @@ -1,2 +0,0 @@ -json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one -package_name("cloud.pablos.overload") # e.g. com.krausefx.app diff --git a/fastlane/Fastfile b/fastlane/Fastfile deleted file mode 100644 index 19c557cc..00000000 --- a/fastlane/Fastfile +++ /dev/null @@ -1,38 +0,0 @@ -# This file contains the fastlane.tools configuration -# You can find the documentation at https://docs.fastlane.tools -# -# For a list of all available actions, check out -# -# https://docs.fastlane.tools/actions -# -# For a list of all available plugins, check out -# -# https://docs.fastlane.tools/plugins/available-plugins -# - -# Uncomment the line if you want fastlane to automatically update itself -# update_fastlane - -default_platform(:android) - -platform :android do - desc "Runs all the tests" - lane :test do - gradle(task: "test") - end - - desc "Submit a new Beta Build to Crashlytics Beta" - lane :beta do - gradle(task: "clean assembleRelease") - crashlytics - - # sh "your_script.sh" - # You can also use other beta testing services here - end - - desc "Deploy a new version to the Google Play" - lane :deploy do - gradle(task: "clean assembleRelease") - upload_to_play_store - end -end diff --git a/fastlane/README.md b/fastlane/README.md deleted file mode 100644 index 7ec1207f..00000000 --- a/fastlane/README.md +++ /dev/null @@ -1,48 +0,0 @@ -fastlane documentation ----- - -# Installation - -Make sure you have the latest version of the Xcode command line tools installed: - -```sh -xcode-select --install -``` - -For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) - -# Available Actions - -## Android - -### android test - -```sh -[bundle exec] fastlane android test -``` - -Runs all the tests - -### android beta - -```sh -[bundle exec] fastlane android beta -``` - -Submit a new Beta Build to Crashlytics Beta - -### android deploy - -```sh -[bundle exec] fastlane android deploy -``` - -Deploy a new version to the Google Play - ----- - -This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. - -More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). - -The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/metadata/android/en-US/changelogs/100.txt b/fastlane/metadata/android/en-US/changelogs/100.txt deleted file mode 100644 index f3017522..00000000 --- a/fastlane/metadata/android/en-US/changelogs/100.txt +++ /dev/null @@ -1,10 +0,0 @@ -- theme updates -- layout updates -- implement bug reports #12 by @pabloscloud -- backup data #10 by @pabloscloud -- import data #11 by @pabloscloud -- switch between years #9 by @pabloscloud -- fix wrong fab shown in tablet mode -- fix "unknown day" shown when devices language not set to english -- fix time formatted incorrectly -- fix toMinutes() not being available in older api's diff --git a/fastlane/metadata/android/en-US/changelogs/101.txt b/fastlane/metadata/android/en-US/changelogs/101.txt deleted file mode 100644 index 00bf135e..00000000 --- a/fastlane/metadata/android/en-US/changelogs/101.txt +++ /dev/null @@ -1,5 +0,0 @@ -- fix navigationDrawer available all the time -- fix imports and deletions taking forever to be done -- fix sheet being gone when swiped away -- fix sheet not opening on same day selection -- fix calling click action on empty day cells \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/110.txt b/fastlane/metadata/android/en-US/changelogs/110.txt deleted file mode 100644 index ecf3bc70..00000000 --- a/fastlane/metadata/android/en-US/changelogs/110.txt +++ /dev/null @@ -1,4 +0,0 @@ -- add goals -- performance improvements -- fix fab shown on past days -- fix weekdays not visible while scrolling \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/111.txt b/fastlane/metadata/android/en-US/changelogs/111.txt deleted file mode 100644 index a863ee1f..00000000 --- a/fastlane/metadata/android/en-US/changelogs/111.txt +++ /dev/null @@ -1,4 +0,0 @@ -- adds german translations -- adds ukrainian translations -- adds translation link in configurations -- fixes a crash on startup on older android versions \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/120.txt b/fastlane/metadata/android/en-US/changelogs/120.txt deleted file mode 100644 index bb80c76a..00000000 --- a/fastlane/metadata/android/en-US/changelogs/120.txt +++ /dev/null @@ -1,7 +0,0 @@ -- adds tracking over multiple days -- adds themed icon -- adds indonesian translations by @mondstern -- adds slovenian translations by @mondstern -- potential crash fix -- fix all years showing up in year-picker -- fix issue that would create events out of nowhere \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/121.txt b/fastlane/metadata/android/en-US/changelogs/121.txt deleted file mode 100644 index aa7d67f9..00000000 --- a/fastlane/metadata/android/en-US/changelogs/121.txt +++ /dev/null @@ -1,7 +0,0 @@ -- code optimisations -- fix issue that would not show the year switcher -- fix low contrast -- fix issues when importing backups -- adds dutch translations by @Vistaus - thanks -- adds polish translations by @mondstern - thanks -- adds russian translations by @0que - thanks \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/122.txt b/fastlane/metadata/android/en-US/changelogs/122.txt deleted file mode 100644 index 9dd2be6f..00000000 --- a/fastlane/metadata/android/en-US/changelogs/122.txt +++ /dev/null @@ -1,2 +0,0 @@ -- animates start/stop buttons -- improves calendar for tablets \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/123.txt b/fastlane/metadata/android/en-US/changelogs/123.txt deleted file mode 100644 index bdebcdd6..00000000 --- a/fastlane/metadata/android/en-US/changelogs/123.txt +++ /dev/null @@ -1,6 +0,0 @@ -- adds possibility to manually add items -- code optimisations -- updates german translations -- updates dutch translations by @Vistaus - thanks -- updates polish translations by @ewm - thanks -- updates polish translations by @SomeTr - thanks \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/124.txt b/fastlane/metadata/android/en-US/changelogs/124.txt deleted file mode 100644 index 2b663e20..00000000 --- a/fastlane/metadata/android/en-US/changelogs/124.txt +++ /dev/null @@ -1,14 +0,0 @@ -- adds possibility to manually add items -- adds animated start/stop buttons -- adds improved calendar for tablets -- adds dutch translations by @Vistaus - thanks -- adds polish translations by @mondstern & @ewm- thanks -- adds russian translations by @0que - thanks - -- updates german translations -- updates ukrainian translations by @SomeTr - thanks - -- fixes issue that would not show the year switcher -- fixes low contrast -- fixes issues when importing backups -- code optimisations \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/130.txt b/fastlane/metadata/android/en-US/changelogs/130.txt deleted file mode 100644 index b1855425..00000000 --- a/fastlane/metadata/android/en-US/changelogs/130.txt +++ /dev/null @@ -1 +0,0 @@ -- fixes issue where dates weren't readable \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/131.txt b/fastlane/metadata/android/en-US/changelogs/131.txt deleted file mode 100644 index 11a3d19e..00000000 --- a/fastlane/metadata/android/en-US/changelogs/131.txt +++ /dev/null @@ -1 +0,0 @@ -- fixes issue where backups couldn't be created \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/140.txt b/fastlane/metadata/android/en-US/changelogs/140.txt deleted file mode 100644 index cb0fcb57..00000000 --- a/fastlane/metadata/android/en-US/changelogs/140.txt +++ /dev/null @@ -1,4 +0,0 @@ -- replaces bottom-sheet with separate screen -- accessibility improvements -- fix minor annoyances -- adds and updates translations (thanks to all translators!) \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/141.txt b/fastlane/metadata/android/en-US/changelogs/141.txt deleted file mode 100644 index 8931aa1e..00000000 --- a/fastlane/metadata/android/en-US/changelogs/141.txt +++ /dev/null @@ -1 +0,0 @@ -- code optimisations \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/150.txt b/fastlane/metadata/android/en-US/changelogs/150.txt deleted file mode 100644 index 992e075e..00000000 --- a/fastlane/metadata/android/en-US/changelogs/150.txt +++ /dev/null @@ -1 +0,0 @@ -- adds ability to edit existing items \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/160.txt b/fastlane/metadata/android/en-US/changelogs/160.txt deleted file mode 100644 index 39c6c312..00000000 --- a/fastlane/metadata/android/en-US/changelogs/160.txt +++ /dev/null @@ -1,6 +0,0 @@ -- you can now swipe between days in the calendar tab on phones -- updates app icon -- updates splash screen theme -- fixes multiple swiping issues on tablets -- adds delete forever button on tablets -- updates translations diff --git a/fastlane/metadata/android/en-US/changelogs/93.txt b/fastlane/metadata/android/en-US/changelogs/93.txt deleted file mode 100644 index 7e5e153c..00000000 --- a/fastlane/metadata/android/en-US/changelogs/93.txt +++ /dev/null @@ -1 +0,0 @@ -Initial F-Droid release \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt deleted file mode 100644 index 658ba3f5..00000000 --- a/fastlane/metadata/android/en-US/full_description.txt +++ /dev/null @@ -1,11 +0,0 @@ -Overload is a user-friendly native app designed to facilitate time tracking for everyone. - -Features -- create time spans -- automagically creates pauses in between -- delete time spans - on-by-one or all-together -- scroll through days with ease -- backup your data as .csv -- import backups - -For more information or support create issues! \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png deleted file mode 100644 index eeb6ecd168219b508cbb89f7e94a52161bef0c5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16749 zcmeHvcU05Mw{M6b#e<*-s0aiVL3%HtMFks%A|0ekq$fh?Ekuq7D`25lu^>T;AcPhW z6_GBzmqYKNcfyQ^BUD0qZISg9Yv_mXD82MzY>g|m>%`ePH|%V(7Fr5i{jLJg zMkbP{_Um8ek{9y=WPq%}egGU#XyEV+)#Tku-d#fD(HUbWwsm1jMMXt%2`O1+)z|g)5l<(RsW;qDU1Jm*ni@HQnz+TsZz~NR6;>i zLQa3z+!4R-04GWjBs>*U@1;bV&Ep z!G|5sQwH1noSbzf&m&v1>}{M6Yl_@0w`er}dgt!F#tzLxy6Wl&_G$Qrm~(o1jS^iK z{Bf$E<4{(1F@>uU+jhNJvf|j{M*Ns^Np5!BxH4hrOR**MRv3f^O3T0sLj*zahZ#;@ z*Li-B@!Pj=&1=LU$bjI_?{^*rL0F&P5RUjk1^WN^-4CSqOPWrwNBbb> z%PT?-aX(QIEl_mT%w~IA!lt04BI2E~GG9dBOf>K!h^29p2LBxKYk6Sezd5*Q%VPdp4-2!BWc=b7u>xB+ z{?5NYYvyjf++5zV5jL?Se^(oTTvj%``j{^Fg}!aaD+Gg6G|+kWaUORT)5t5rCgIM7 z^AlWQE-EhLn)LC-x=|#H?#ARRg%|px8PAnPRkI^fg`;+P{ z+q+~{z6P+dH-(I k}=9gy5VXbV9x< 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

^?!o?hE%pT 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 Bk;QBGZl0X8|8tv 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

#}$ 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