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
- |
- |
- |
- |
+ |
+ |
+ |
+ |
- |
- |
- |
- |
+ |
+ |
+ |
+ |
- |
- |
- |
- |
+ |
+ |
+ |
+ |
## 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<
zi|(UcUi9)ARH{I?nrkhm2TR$;SNSA!shRfXe9INnUX%xCYDEw^W?dUfn{omMQBRE$
z;zJDV>~m*;@2!jF{kkC%a)#
zRA-e~B+%rg?Tc;F=0t7`ncxb%2a`E{x3?#cWDpVwMY;KU5^B+$Yo$6RVRt4O)wK|i
zJJ&SX)nTL1Zn|!|gf_F%zP%&ui6xzZg_z*-6I{^?eAHf+Ps
z6g6Vl=?^efW%Es1e0@ePaL_-53o7w<1w-wge@SiZ#NKMMwPE*-1_v_dn(u!7o
zJDmSGhL;8!yI%0BWQ-3?f4+WM^cxm%-0s
zys};wxEa^vYUW3`tUS$ckJ{FLj&wN=?LY7(KC|{xO?(mgkBz5*j_wqg)%c?`5ZnXA
z#8%T}%HIMj3Tdy%e+Hf@8|qd)cFcUIF@yyl&*1L|7AuOKloF5-aEhVQDsr$w-2QO(
zh0-21PZkv1e{lQ!N;+D}zn^Ziw`-^=MNT8(;JKp#+-wKe5)q>>P+I1rw0rv#Ka*8P
z)j$#%J_dy%66L?F0QXahsM^CHCJ@EK?TJ`@SUMr~x~5a~eBch_4iXjxjnRuz#K$y8
z*R<9mK_2=Un99-qmIt&9!}`gKCG}jg7)9^t0hMAgQTVg*cb{Su&FhD0O9jFeJ!06R
z6(9Edy~iDdAz%%3v_Z<4@R}X(F=urPt0)J!&aHS^
zF04;7oNUmx2ro#lnC6%m>~1Ry2tLj3Qnd4Wb1qUmZ>kYZUeH}u+wwq3xO$9M^-9-~
zDea8M6auF@x%aC2_2*_?F*D57t{c$5G;1g$4RF;_O$5YV1dv1`XAkFTQ|8
zZ7z?yzW+=w(r14uH7%iA4#Ni52&JFYwhCD5aYywtcT?V_XjcUD$BAc}D?5y&Dq6HH
zQ9}dm57&F5)|Lsxq+#O$OEny6gq=luYw>H+()jw-tjG*>!~88ZiGaF(r^G8H*&E|}
ztN!cd%rD!caM|`Vdq%uJ}LUxCeK4b&xMn|RjpK97bLm6+10N1{AO
z$R!lin0UWs+ehB*_E^v5Q_-BY%ml;GYl-wl|rp1xOJ!J23G2JpUGBRG@*VApa)&Hd{k6$qbGv5%P
zw7wF74fNg4Z{P1*)}L~LWBZkFn3d0VCH&6uhkE*F8gg8}N_$%kF0zFgdw*d+7g$_Z
z(9E$EYiXkLdcsMlvux?k@cxXV{H|y2*3DUH4R8Q&VuB#qH_&~@5mwv`Py6&Xj2k7Z
zGOK3qP9O`na%LhuGLPI`-74m$m*{8L
zXE)s{mx3oYZ&^QU7_)*EI=J2c6yHR=tR@6kqHm6SO!XuOfG7FjcojwTr4*yQ>wiM*8Hb*Ttdq_L5oEf7GOpfe22Ti7mY!HTPN2XwV+
z2Ij53k3b=dLUM2A-lE)PW+UCYAM0*@{{ReOB}z*hl=ps7hz9zIb8W1Qve2-W+n*jM
z?>n*fGAwJ!EMzfjBx!HbUd>m>BwuyfLM?v`PJl*D(rC=~=?U+jJS^f;Fn6R67ZSY=z&;
z0*nUC@I|SUJeT_C>xBE2ExKjB`PTd-_FM^MdO>+wblO}`@r9e!L*7p@NuoKzP}+ry
z2*{V`H#(STpzEi?OCh&<+d#Ema9KXERVPzxByIJ^(^6fT(r(>e
zw4>{CY)RQGa!bS~>bUOb!K2V+(UT?9|`#)~D!37jqQuT)0XJDG@-
z#n+)H$bqs&bc6>GJS(22Jo3!uD3=VcevW1X(l4>yv5Ld)kLZrsoHNAzj*atGi2|#~
znkTpRqqvDqa)rncIi;thw!uLTdCsKBH8UIM!NN~;clQP3m&q9RcP#gA*@iQiJbzRc
zzlB|x{}i2aKtul2U-Xr~B1!ltn
zkiO~?sw9kW)>2=Dkzt~_U-#p6&Aw}e&07(Rlyzjk`0SR=c+S-41j$UdK0I$9{MpV2
zOWb@FXS=wQp6qkSYjri{e{Lvp$TTL3o(n?sF)%=Unwhxx!Zq=qJnnMc;;s>hYB=3T
zY^VyL^u21IOp2?uLv8NpLEOfa2fsboQ(QyD$a4HBX
z9QO|F)YHUVGf)~CY4}T2W#OgLj;`F|Jx)za1;5c$z1jGjvBwSRn}%MQ+h!YEONHfJ
z$Wpa^2Vq}d01)-W&Fld}kLhG$+Ro18DyEQtN`1UYpFpfD2^HOc#Oq5QDNtsuseHs-
z&4FmNxmZ)&w&S{EjqAZwXByuJsn{|0V@H(BZcg8tBG{2Ox{cE9TsGE{D{<@VWgu+8{hDx}8HUo`e{HR22WD;GMFHcXrL
zvDYaw^1*B)v9iI;Q*nhfG~0X9_3MXVp4s(9t%!t8IhjL`FgRB(g}psGVq+O*o8xQ
zWe&lFtMKCSs9-X8iBEf{U8($ZD~0}#E~l^kNo+Tc1VYyrIt1hk(+7v{vGb=6Zw9)0qcL4ygO=e?5n|L6K7|W$>BLc5p6S&@
zZY?CRP<+NqxSL4LT&cVwOOw+{h$UKodYG?@fv!3&oWrZ9v@_H0jRMwi$H~{ZZ>S{F
zJXmX)x9-H%L@5T=JCENJiF%{2;MXT&nl~+b-{ubm-B8j4Pg4&{%E
zd~mT|!Txi(*NQTBjy>4SYg=Ug{-^bF?gOW_kI*ux>%*{m%si?EB~t?QB+t3I#L9?k
z*W6HUzUnmw9C#?loKX-{flofq89e$7F}dP%C3?$tDj@V2&bD7wO(^M_h2hdAT495O
zR1pa#Sy&h>eRyQXqxe#r7!~I4FyGF0$}3J8H@`G6wGmc*ha*D-WSBrJ>DnZ^ALb)E
zF*Rr*-@gZ*@1N*NFKK`9kUrm6rfZoH3cbWAzCxs)&EwUFF4&4cY=)fN4)V<#KPWv6NPT)o<%Z%5+UjnCy$l1dp
z_k1fRPat3$(5jioYWq26{MrN6I_IKSG`m*?)lP57riUZ=K)AU5mEZ3H{=MPze8+Z5
zkA>e-(aYr!NkOB#v<#mvCbuV&C>@T2hPXb$xy?lz{rszp!*7Ei;gxl7^Sl{O@?24V
z+JPl@d+}ydL{a878~jc~xAFY-&w~)%JM1rd>;*T`&O9vAw>-
z$eGr3JG1rr%PGuuM=}q0(nDk;6EHSUXpET{cBmrLn~v>^G_Z|^qc%$OY~z78tMo#l
zJyxmAfqIfB16$LhA@~9YTXbRVL(Zt!)=Mx;kVgB@JP4>bpWKic_rSTibmX?^j*gUq
zoQCjI2)+Y20_xb}LG~-K2HLGt1IkW!WFJPxd4%@zNV)xd8baqkuy%%I5OThCG7hXL|o%ktBuB&j2Y?|@l`_kK>TnRmk
z;u*1a=rZGTf{E|p>>x=xT~;w9`gOYJh7r4Y|Lnr@`KU5(3}Y)BjFl7bS!PDg8X?=F
z%L-cFXhGKn2C^U{mfF*Ta?mgn6)e&T1b>NkR`TVArz~aJ*^&%sBu))?u{+w-+r>;C0@4@(7X@saMS!0&w*k2EA7^^x9LS
zGCP$U{e*oy7P>yYUa9Hw65lbIuc;uPWNS><1z6(~5>(jDDqA&!5QpfhgBOw&F1F!b
zw7n^bsjTfuUYKhxQ*>#nA+z)9D4$+ml#LVc#pJE&X&|c}s73a{t;W**=t#E=rBjTa
z-vcls4qyplXgVkd3o^Z9?ArTs`9QM5YxCR%$Ca$kyxDw*@^@D21sf)}tj4~xnr?GD
zJHeICW89jBp0e5*2pu~!e(|6uX2MmLjq5oi0UiVOMi_Pog)UR>ip*fW0}8;bxqk`EC(#mc76I-v
zX%P)ZDmsg{sxvhfKySi@@zr+evl%`q8*e60IBHk59t8_c<^xK@$2Ualc1maYa_dkY
zmxxKuaBqatnDYTPsu_gQM*0^GX9*M2d{ZX#yY~CM6E)*wr6SOOc42W@B?&FN{l#I%
z$h{#opmQHsm>a*uc8P=8oAJ2VryH=|(?(9wz(?&G*O#~Nhoc0NN(#X4vmsh
z0W@t1AZO|+!<2Xs_X)GXbifsv!d}|8x9Q`
zAAx0u(s78HuK9Y*gk%UPzZ*#NC}FRm?%3}8(2;Kf$To#S-#beB+(FFJUbTCrrOCSz
zCO~OKjsE3P+&&Yns^sMq69Y^MDIQ+fDMh?C`q^ni#rP2fPX-3k-#Vv=_c%2-Yfbi7
zB`+SqT-lkqXV>0&O5_Ug7RLZg&j*_bon6BlF^I)>ioY6atCG=$z8}2jF+hs-KfC1&`jgttNA2>=sX$*_U%*Z?BP!MTIQ7xYn;S{_39U?0^1EKuH{7H
zwaj)|H5zF82^i!}=wmwunMa7$X#$I*#WuF{qY};#Ufeo;$-qda@k1mZu#D$Sw&?5&
zdzJks?GgwahR95x=enu#M`)l<{D1*_QWT?1CSqfCg6kH`DP;xg%G*X;Bih=5%N1>T
zsqH**7Fn)Fl3Y|J69_NNlaez4^)x!HQFp8Gr+wiTX$8<
zWUIW8_o=8LQzld9AAL3JA2@*jd*1~vNvQ>cTIR^Yy0#ma$p?Y(fiO4;qbIXHp}E1G
zXIhPT{e6~m?w?P@PL1&9U7PE?)!2HX4Sf|uRTQ98aY9%p6gqpO_zgv_W`=M>;HNz8
z58_)sw8Sr+7w4H9%#}_j$6+yX+STd2uS?&ey`PAn4*+j!%NJglASG`w+E(3TWpd?j
zfS+Tj;oz*Z=7BF3$weL><}=T|Sc;~yt1Q)eoogE{RpNy*G*d0>15aUqzet{^1GSq`
z&*~oi7_;<#3Zu(b?@GL
zMYku$+}^(eelfkibYaf*qqMxic7g%UDWm?6L_UO-IU@sP>T+KZ
zZ4O5?_V4)K^tPS4+fh6V^#smTQb9wl*Htz>GCg;0;+K6?bK8fnt$R{NQOLD{Q1ehU0+wN8tHi
z29Y7dC^P4K?S#I)<~bLj>3lj(uU?qBJ;x8x&-Pe1I;_U4+TLY#MJnV*=Tu<+rMK`O
z*Vl$IQ7d-0#Rzx=2|*}1
z@YIm4F|*RMK?pvkwfzV_&$>j$H<5_OA&u(?Rf;@keJ|dhBn7@(eLAbTKk)B?eZHi(
zol|kE78rx7G5*CToNwCHd5$AsDNtFGBm6~X%+gSps+m6AH>|PUzjUaBm^dX(s(YiD
z%abLNrSt-A?`x{AX~cmy29WhFx@>UhMEKSvw|;e0F{lBJEaGIGLYkEty35czm2%3y
zQ_cB_O|CUs&*9n*r6(0-d~WJ+^Rh)8yc36TyyEDtqq;h-7(nPM@#^*q7!8}@elMvHK?y;Dd%;MO?f#;+P;5gw)MPs
zhcX_?4fp6QvP`PWblh}~dMu`fcGV~*c4r9g6fGq-RX(mfiN3PMH-9lj?h#Y{#cR<8
zf`<<~o9r}}Qc{`O=@{PtoSB9YdY!(5GUIK*iChf3XvzIRn=|!EdBy~`$?heKrN%2@DZTyfvAI+690vUN_nQ_tcqGxSiYR1lyXi&N}@1?-mk+EZGl{5((5dg}%d4#A^VKE?@rzt}apG>N2(byNj34
z0@$l_^j8=T9I1L%st~x?-v!G;Airoj{0n+k0RaQlgb(aa1i|ZomZzV8%nY&u;5r+p
z(m=kpOZO+h2G3;n{%la!24LPo+dzGHA_Pny#PyewKW0Y2d;vgM@+E+5ZI|vDXc2t)
zV`h+BfI3o2yB<5R#D5t5C)q$)e~Sjl@V99G(Z}Cf^V^_)`x>en{q2T#1IOZ;{Mh=QCr;#<78U|?W~k`khyz`!6Yz`$ViVIe>zL;26?pcAG6RG9cY6=e`PWAR^ab_wud{-gi;<<3o&&&+M8OU~!pgwLzyhj+%m)Ac
z%xs9i`+%&-hWz^)MjzA;>}4za26Te6kx;h>1A|BZ^#un@O~(QCkzw{(%|T68hT8yO
z#h_;h&^Kamv9kFE1;*>b4Z5^4a?m4jv9h$b=XT*E`>O>v==xVNBN@qGO&l!v$kb#N
zNJId3MkE{zObkq9{BKD}NOfm6*&B*BN?9AZI$^fu4VPxjw
z;$mcCVPs*U2eqKLceQrVbD_7kC;wL`fA=G5WN%<+X5(N6uqOG{ubw`@(SeVQ?AL?-
z`TPq{BNwyZPqMcE_pm?%Wc*db$jrdR_`iWUm>K^+z<$;IKfypW`e#`G<6$m(Hva^?!o?hE%pT
zbKqzBH^_e!|0k6C@B07kfqz#2C#bxgnGtBx{DmuMXn!I2XW1X^K`)D2#Lno~TKWtC
zSn~gGON*EBKU@7r2eP2KZ>Z;>C#vUQ#LvRS!b;D~1Uf!5b8)k((7%@+|J(2O2!UW=f?$%OLZ4m0k6MD_H2Xbw
z=U3eQ#N1EEBE<66Tbx-%>;f?;6{SP+h20mL=9Dy-Rx7aG-BF!w-J8DHI{Qh-pmnbg
zfwAHY>8`cFEV!?lp0+fM{)A~REiJWXbNi~Vcc6jiQ>L>$m~^U?!q|2{sPi(>R7}z@
zO^k{H{`)x7h2ST8I&yRPT?DFTf|Wr3Nfi7Q_MgLr8{AA^z6{*W>(7Q}`qO{(g#s>5
zO(J`=&n`I12th{j&mmjP{rd)jY;7MZKZLIZdhh-Akq8DA9sGe*@Yf>rXsVklyzE`ikAZR4`S|@ox-&Vb?whCxPznhB$T^_6HsyVj=xQDbU4#iS-u=|4XcY
z&9DE?tiKlL*?*VeUrge^JnJux`dOl24tpf{lWpZg$itd<(w-rvN)h
zt!CeDDKBrGcz}V5E-G8y0LsZnZ*Dz3Jw32YDgPiu0nqUcAycFbVnX=-4Tzn?3aO)@L#_b_E1@xLGPA5gbGQ
z5WVfr;a?Zc^oapki1=t1m+%{#oXN#J+zUT{{%rPao(;-GASWlcue7(bzZ@nQ>ngFb
z@^UiXyxuPU`KMHG#fRFGe_FmHyvFzw-9wPx(Cp>Z_~{SPyVfKmn0nWA&T~NXXAd*-
zf~hXgM-B!512?~~Z0F9K&QnT%is9lcQ0c?`zSJKoi_a-ExY@>O&*`_KKUD-Z0r0vo
z`Q{gK+vx=O744&n-NQ}Rwl#JEf!*%c=9Z|sq_QZ+ij>_sdw|!9`*7XG&CSimtEUGR
zmiB394R5H>*XEW8@M+$AM!Mn_U5kDlv)?$kcZ2kuS*WGiSw>!7UJeEhUNV-cJO3ak
znBk$3$jFF@2+&U?!qqJz0)mXJYzZZgj?QXyd3kcO+Ei&uX2#D)x2EAiPY+LEXXobT
z200nIh%k_hymLOy&0OCu+3Hp-9RK?ET24l${4yBW68%HSM~Jzif_a>I9Thc4Z{5eo
z#~#DZ(9q7@&d?>lsLtKRC74w^GKqU@YfB!ABh8?yLDzCA@GQ9etUNe+4P_!iK0GQi
zGBPRx353+u%}8;?*EsyW_}>J7ItS^8ZN7YkGZFac@bGW#FW<(-#^vSaiiik9KtM2I
z=1G~Dm{3zwi;D#|H8tVmAJV=bNK3_|?&(z>WqbF*MM-abJg?W!*LNsBBZDx@8W$L
zn{u9u3l1Mm;SAhI2&al>YiMZb>S}6jO;_B+-BEcOD31K#=DxeTb5y4FJh!?kpl6`J
zpE$A9>|yFJE(Q(r=JEc)Zu;nGX=-X~Z%+aMkQY%h_;}CCL71MAsm(C2+7{4?`Hy1h
zBR~e4I@4an%o+E>||eK*6P
zVk}#GxTfVS)e(tNN**gO_k)Q#TBzZnq+}-}L%U?E6Rgn)$WNA(WL3wg%znqn_&QM#
z=2*k7*(60;QicO27@<3*Y2Xe_lPR4`bXqF;SGJIQnN
z@+#~v>87U>CMPFVDz{-=Y$>Ryj^|r$+dD+|&2Yhe?
zcGAioAGAmDTAIPQ$^WrJAWBI=#)&^G8d`UQ^Xki_$x1I+`24htzYs>!>iIb{UGX#E3gN}{weXxbwMLR)|#!fV%IZ`uM&IMG)
zU}Rzf10S~H@}pv)U5q4j)-}y;DRI;bVE0{VKty72SIPK;qt%gv274Db=b)|Ize7(g
zmp;(fC%QHNiH_vc`Gb|E`O*FfE*_pB1~e43e}Gt5cTeG(a&%&3`o|CY!`&*4?xU?C
z{oiB8V@^(XT~t4;7|gG#sZ~{Wazz+ei;IhExLbJZ5U&6yt605Sy@D;bSUNG*!@vss
z^AX;@9T^edQ%AolqZDFrrAq~(uy_>^%o7wHUzK)7xJvljnjvgmzsnoNQ
zoO?cIYWnT;v2F!D(zpZi$ks^){+fOpCVCOnw#r#5q1N?Wb$FLfFH%h*Z1Ae+H;PMG=~~ZRjh+1hDS!cF1E%U
zyxTj2C3o{v=UZ`D=$8u_dd$#|1V%k6hdJ>8K-
zMMa~NFSd3uj}}_$it1n@O0S&;;SR(9;ny0dE--8rIqOdzkjbgu?OCJu0DWqAevc
zJjSzxO-@PH^dcd47Nwmr;O@KkmstW{Pbm9lZ&-k~dUpDDHoDn)RgjSML~nwSiOB=;
zLQx1)qRY?s#tkqrN6+MS;TC95`I))S&({4I+nyjeb<&B8n{3w0XBFh-ou#EG$Hqu<
zYs<^~roQuH5W!QBly`0RMtR+iwpm$QcQpaE{g7M{c~1{4t*i(M37;co2M6rZn^Hj$OQ0>0FywIvLczWGbt+bxpH+&rA?92cMUhZmn
zdh+S5uRq=#?VdM49q~9FZK=DtH&OQ_b@i@kc>W=gOJJKJQTF@D4J!?R^No;O$hQbIGqcZ=gIPR|R|!NK1kACVVS
z+Vsp3-_gxg*Hjmm)xq#R3^Z|aaCq6@li{Vdn+>9q+ZPoTTCX$*CPB05Z*CN`)w8Wl
zou{q7;2pAT%y9zn#6F29#+veNxpr{*0n=YcyD&nwmOOizSVo
z=WOfHYP-qZh<*!ATL1*Fm@D7p;9!f7k1r=0f$D4}8fRukqPb_Kq)8BO=bC#8b#(vW
zqj`4`wyQ-rh^}E0eKZ__$O%|-d2K<~P7Zy3J|AmO!-X-+?Ng_5{KI>6BBonH^u~M2
zj(lA~{qWU>FRO+Z;&5xME0JUeO{JNS*4y{6la{eJG`yg;vC!?HASWr!INxN;aCc*>
z>0w(f4w04P8_n6^O9mQ8}}c~my_a5&Qu@U
z7T88c$9oeOX=j^;
znu@Bohq|P%7spmT!X$VfQ~1aHE)zc&GY@Ce*+i623Fi}}4y+EXx{c{OMYi%JbRi(`
z{Nkd9_POOkifW)^g6mgoaUJB+?$MHsfI$C`AG^x|St3%;G{5Db^~W{Bvy66(P*pJg
zro%7#CSz1X5Iz`88l{n$C7ZBnV(;K+amVp9`CXSLpUufDT;X0Ik930okc(65DftB^
zPyuv1W*IuG;8bciRjj-0j+DgN<8e}rMFTscSUs}i(^LCKn_(#f
z@2~H=%)@T!r4NfQZ7Xa=s-HEaFsuItTM{0b2{0TQy`vD3=&G=;FODv
zg`13bc8d8R-21e_IBFeLnj5)I0*W~G{YRVA-;VZ8XErg
z>^3Xi9cCs)@r#E99qe2zuA;})c+|`mpZ;Q1s-t6Al0nYxB@lgyC^+)Bo3wrTFUGa+
zZg}3l);<&;CumRxB_*g+%hNM`oj@~h<+o>W^Yc8oxrvJ2G>JVYVBQUQ;=kS?^bKY)
zw#HdkoO3|ZR1Y2^U=^i~l>7ZcrOcwh!i9n5=$3AX?uAbq>Q*x2Omg>yjHB3ZOKgSZ
z*I?U>Aj0O+mphHuG#%b0CwJ98nlrZ~2P9!L^pOLBHzqP#rv0LiWlLvsf@_-=X8K4d
z{fACoYj(w_hV2~|DT~X|;h_|NPr@@oDv-fFR%zPXwt_hmuK`&K?
zl#=~URuyY1Lo-G?3JQQHUmYxL2-c%clOy15BZW3bN}t1JB}&uL?PSD1Onv4f$Sy?1
z7se8Z=x%FiPQIxVj-Aia*~ZB;eV3+BCo2S1J*zq^U1hVZW2_sB{TAor;`IFZ%*svU
zjor^?M@M#$I>&;hA?WZAvsnuS*=^X~@eTK`xo2eIEzR1+aPUUNTSnspSd2DK7#KJ>
zxVT1bhcMjk6K5UmQx8ZUurp!~dV}RW%v_4v%IDKbyXSG*I{Wj$D6NlMGR^#MjZ%M;
zwVnM}=UnIzu6Ftma26jkgCir2Lgw^C*$D#t1JN3W_({+G;Kn$f?d*AeQ8ig%jE=Ti
zh-m|`CJG*xn7C7+TvzJ^gi(z7hXSIy$50p)7BQHWj;5xt0Nq23^+jPI9Q_ksd?&CI
zJ?X?>JQHYWQNPs+nFZ%NiVNw7jWU6o318H8bx9#b6?tBU7A&!bfLPqsDDex6Xq^7Wx
zk(6}YW%+6OyUR0rkn5Zgf`B;7)XL)U@UTj5ovYkvFcA^1Yz}NxiWn0!BPGGAt%B97
z8WVFnFE398k5d5W=+wmC7(zJBhl-?}nc3`c#>!gT5>x(fG73VqB7%&vk8b`dy41S^
zp%>js+S=;sTyNe41ZzFU$whfSd&DVyx`2N>4>KY)wm7SCe$+qLlotZmAAZY(G^PTJ
z#Ie5E+>oDb6L+mp#v2?Q{uVy!%yW1QpT?mkMP+&Znr?MhJvkqR!V@uO991lVYVkTP@uu+c>b-OlN3i
zOfou8^pH}}(;s*BvobR?t1N9`5_k0Su)Ke0x#hp(eBJS$uW^?*{5;iEsoozdpU?OY
z61-nQ{)|{+S99|pAZ>1D7#}K(sjYo31tg#>W-~#yowA{=gTqj5CE;S-X?SD^`^{cC
z+(u;N%}-T^h=>-}Nc0_Yfzt!GtG#uQvZSK0US57SF)?A#E8hc{YZr5tj-G#BKio}<
zPxiRlI|l|u9nDvr=0$lq+?|c*=Tm&H(Rxo9`aA5gh#IY?ei4@q0f%-z%&B8yY-nU`
z{BVD7fJ;e+uRpukyZh3@gNS(T=LdOdAEx2%@9)26+hioYf3oZjGLq-3{0MA$+n?8Z
zh7S=V5EGlV?Wj3l4BX5Q*8Ltqb}%X`s;Q?`=r#|(H8AEcX8|$4$Jo+RFlX?#rXE#$
zc4kbBa;?>4trbsfoYLCYQ8-dMZXO;tclVNy?$%FB?^FL#fOpZNshpmUO$v)F%*`2e
zT5+&$JoPI`VeTm?DPi8gTmrPLWZm7xa#vDl|CdyQbMd!L=C+&
zH+N%x`e#yaZx5Zt&&`abJAnVu^`Yv#hv8!FaZZk2d_4WLLVr%L@Z`kA;h}{j6ALd3
zJ72e|kB^X|qGG>fKNnX6SA%Px>7)+CY|MJhI>qiNhPtZB
zFEios@jf9jvAeey1{&JU{gLF3jEwAlbdc8b-UwbB9^}j*CkI|%JFaRzJ)A5rTEf-w
z&NtaXw>)m0i}XOkEPUAGhYKiqr-&
ziYC>6E3-EEps*e73kFD6Bz}F#a&K%38A*#x4aMleY5uhK=EU7)?4(0Syg3
zW^Rmx!!Ir-ev}kk5^wMB{y1-+rJ|y`L0|s;8&1;v*q8zVjfba4d9qtfO%CL)t*{(nhfEfu+-#hj
zWFHoLas=MJ0|`N0XTHiW&rW&d4KZGA
zblr7rb$$49!+uc_%d$+26P#ER1slj^?_pJ$Y#IE)C*T7eTR6LurB+OODjp#&CJILI
z4v=}xRo+=v7L!Bq^V-%H6tuyqDkeHRE)E6;7M3yWVNzC(4>;7xV4KR5&Rbv=&Z6&J
zX#kuv0U@q-SQfO=PGOja8J!F1aD!8K+Pr7Ss1JZSX?J^PcW0;KA=)5%WN0KjBJ%p?
zYKnbo-IsSH(R0(_Co{IH`3(drW3M9*ET7otT
z82&!>%l3y<56i$$dDBP-F?BrLMCt0j{*u|F#J1eL*&&0rks{PiQ<+JJUeaF)w=>=-H0fskLrRD(ri8J{qC@y|h5KgAFApz^*zm)AE}g2tt9HvQ_ZByni3}dsx9=X+eUt
zDYo{6@^5ttq_V6U*OaXF@M7D(v{G*Qjf|mlV
ztkV1v_mJNV9=4twgp>1=@@J|);Z=WeQ%b94>EA;VZ72MN(`H#N%m09n1))!CcBuVZ
z3m}0wkpSUD=#7~m`6qk=2z@O{jobf05^)etCwD);$@~dl`U`!jg~p^mNy76Br~i`l
zza;%gl}{-3g8qQON_fCC1pP)1vd&4+WHp0Mb3v;0(}zZ?&03G4TAQfBkMl0
zc__I`2i}OqU#(PGle$O?(5tr4(Ybt&ri97P)=!_kXhlOH7H
zLHDe$oSCLr`%6aoRpCmlb9b1%nm*Q!tI3CoKAYH%%*nDuUEriYM3Cd0oJT0$bv|Dk^fk3=OvuRB41HB=%;k7QN3e+rU9FFL!a1$G)?2
zwu<+G_^4;+ks%Sw(a~$Cufo=;Mh0L6I4CHDm?DduqeH`^QQ=6J6UiA`EVDisnW?F!
z78dx#_`BO@iA3~dWEUm^j~@*`$EUn*ZFLd9oIU!yc$sa~K|Pfi9}1;@V5dJmeL3-l
zhh=&garAiqJ~M;walfiae#W+h0Tmq8h0-TEc@A0NX}z$M?30tTK%2|?&(TrO+6QHs
zXKW0Nmoo3$woPckfx^Nbofn{YB9%5_YAdFXv+vievQv<^VI4zpY2XpGBQt25+)`Y^;P+8
zFJ$!f(RwE*mE<#di3kYPXj`?^OIZ7eESj9LBwG!orfRZyTEPiAx
zgc$7mZ*H8+>vs@9{}@-KX00u;D%sp5I^aC=4gIDO3>KIU3IrM&8yzn+;6vJoiS772
z2k}2|ZQ>dhRLPsFscC!Z*-FB_bm92_`0-<4FpJx&d8E{fc2~{PW@9QvtI6q-#mvl_
z+xx{{X4bx{3b1WF@ENN05DLfeVeNG9(mYE16L)~#snZdgk6h?iK5*oTF0Ij~mvhcn
zKWO9Er=9Z-$e>69u0VUVlTOuA%^%;2)iR9@UB3PNX@BpU@#_sML4DECD8DW&C@d-}
z5dZiwA;HS~YEMr@WclK0yaNsnU%D>7N3>U$EO&dQ#r@G$6?ST(UfXua4M4yWD@zm6V)TIN&b9@LtUo}XD#+u@il7yAWdBw+c5n&OK!|d`B{`nb%
ze~n`en?`t-T#>>tqeoM3U{M~jTSnBd;P#f)r4xMi(#N$X;jany2?>~0Rjfin9XF$*
zn^JSpHdlKg!NIVXyk?pFo?q0}`Pq04v(Td6iVcotIk#MY@H5|&u5SZ+yF^oIC+p}^
z>|fQf;0+BTC2gkc(-ifqaP^Ok?Y+_I@h77jT3Q0dOZ97SW@B;e*~bwgDrjjWca998
z(wK#naQlatY>eMC)Ol_XyJCRpj3cvJb6i|eO}$g9v5&N{Y-#b>`w_v$NR{)Bl;*g~
zQNF2<#$mNo
zA4An{b32luAu5+`;czkcSs?EO|9qjZ)09PwoRoJm_F(r)*xK-i#-)aa1*>_R-^ER#
z<*=h%Msx9nO
z#xI%_5Z-n+b$NJP>~3zks`9@PNb9RxA*vN-a3`yZNGqe;^ilQth!eGLum`&s*2k5S
z5S9nv^#xOgg`v=fWkY;~DU{vb**RXQ>6JqtWl3|mJ}}xU^OTD0_gCu(LkumA7Teks
zS7Na*uxd3$wJ4{=FN4&OCfV)QURh71eKl)Fw`^g8;YB+JMK=hWgjupX=40oGmTn+&
zdbL)n_opLvUHW@HMsWZfi?sUNzI$nyaKGMpOh?_L?+FtfT{zPxkh3vHG3G@yNc{{i
zD|zqm@YrkRW1oD`Nzeg49#Lj|G6oirm^;qiF~9=gC?OI83ZCZ~=u3#f=U;A4Gs)yr
zXvhbSZUp*Acnog)%bQzRghYl&OG=WGg|}ttfYPiCls4A{1sdZ#>InhSeS3TLb00Pq
zm{z;8H2YrgL`3{NJw5XY@QGQ`8L2yHNxEtWe0azdo}QnLcXvP@J~#JzdFQWfud&Y4
zm4^|7BEtG=l&JXP=W6bvkUM(k$0vu!d??hAk1`>PK&_gHxQ Bk;QBGZl0X8|8tv
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