From c1fee63735026027ec180791de2fda20ff7867d2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 16 Feb 2025 18:32:38 +0000 Subject: [PATCH 01/39] fix(deps): update aboutlibraries to v11.6.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c412a83f3..28232aa72 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ detekt = "1.23.7" composerules = "0.4.22" # License -aboutlibraries = "11.6.0" +aboutlibraries = "11.6.2" [libraries] # Project From 022fd020c6a79b87b5253cd274fe5fc97796a2eb Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sun, 16 Feb 2025 16:28:43 -0500 Subject: [PATCH 02/39] =?UTF-8?q?=F0=9F=92=AC=20Update=20strings=20resourc?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrected minor needless escapes, string template and updated the description of the application in the About screen to highlight multiplatform support. --- .../commonMain/composeResources/values/strings.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/src/commonMain/composeResources/values/strings.xml b/resources/src/commonMain/composeResources/values/strings.xml index 02118894b..866b9b681 100644 --- a/resources/src/commonMain/composeResources/values/strings.xml +++ b/resources/src/commonMain/composeResources/values/strings.xml @@ -10,8 +10,8 @@ Add task Wow! All tasks are completed! - That\'s all clear - Ops… That\'s embarrassing… + That's all clear + Ops… That's embarrassing… Something went wrong This task is no longer available Task not found @@ -50,7 +50,7 @@ Not now Notifications - It\'s not possible to set alarms without notification permission. If you want to use this feature, please go to app settings and grant it manually. + It's not possible to set alarms without notification permission. If you want to use this feature, please go to app settings and grant it manually. Take me there Ignore @@ -63,7 +63,7 @@ Category Remove category - Are you sure you want to remove this category?\nRemoving "%s" also removes all the tasks under this category. + Are you sure you want to remove this category?\nRemoving "%1$s" also removes all the tasks under this category. Remove Cancel Remove @@ -79,7 +79,7 @@ Task Tracker Alkaa - Alkaa (to begin, to start in Finnish) is a simple to-do list to make your life easier.\n\nIt all started when I created a project to learn and keep me updated about the latest Android components and libraries. And now it is available on it Jetpack Compose version! If you\'re a developer or an enthusiast feel free to access, copy and updated the open-source code.\n\nThanks a lot for using my app! + Alkaa (to begin, to start in Finnish) is a simple to-do list to make your life easier.\n\nIt all started when I created a project to learn and keep me updated about the latest Android components and libraries. And now it is available on multiple platforms thanks to Kotlin and Compose Multiplatform! If you're a developer or an enthusiast feel free to access, copy and updated the open-source code.\n\nThanks a lot for using my app! Visit project on GitHub GitHub App theme @@ -103,8 +103,8 @@ Completed tasks No completed tasks - You don\'t have completed tasks in the last 30 days - Ops… That\'s embarrassing… + You don't have completed tasks in the last 30 days + Ops… That's embarrassing… Something went wrong From cb15855a6d0736246d26d2779e97aa5d3d25b7de Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 16 Feb 2025 19:08:29 +0000 Subject: [PATCH 03/39] fix(deps): update dependency dev.icerock.moko:permissions-compose to v0.19.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 28232aa72..a3a59be94 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ sqldelight = "2.0.2" # Moko moko = "0.16.1" -moko_permissions = "0.18.1" +moko_permissions = "0.19.0" # Test test_junit = "4.13.2" From 004a461bf42c15b0f15878eba82d6099183e48c2 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sun, 16 Feb 2025 16:23:59 -0500 Subject: [PATCH 04/39] =?UTF-8?q?=F0=9F=93=A6=20Update=20Moko=20Permission?= =?UTF-8?q?s=20to=20use=20separate=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, since all the permissions were in the same module, they were registered by the iOS app. That's the reason why we needed to declare Bluetooth, Location, Contacts and Motion-related in our Info.plist. Now that we have this separation, we can remove this. --- .../task/presentation/detail/alarm/PermissionDialogs.kt | 1 + .../detail/alarm/interactor/OpenAlarmSchedulerImpl.kt | 1 + gradle/libs.versions.toml | 1 + ios-app/alkaa/Info.plist | 8 -------- libraries/permission/build.gradle.kts | 1 + 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/alarm/PermissionDialogs.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/alarm/PermissionDialogs.kt index 496804eae..2797af849 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/alarm/PermissionDialogs.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/alarm/PermissionDialogs.kt @@ -18,6 +18,7 @@ import com.escodro.resources.task_notification_rationale_dialog_confirm import com.escodro.resources.task_notification_rationale_dialog_text import com.escodro.resources.task_notification_rationale_dialog_title import dev.icerock.moko.permissions.Permission +import dev.icerock.moko.permissions.notifications.REMOTE_NOTIFICATION import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/alarm/interactor/OpenAlarmSchedulerImpl.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/alarm/interactor/OpenAlarmSchedulerImpl.kt index 3139e6528..ee340b596 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/alarm/interactor/OpenAlarmSchedulerImpl.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/alarm/interactor/OpenAlarmSchedulerImpl.kt @@ -2,6 +2,7 @@ package com.escodro.task.presentation.detail.alarm.interactor import com.escodro.task.presentation.detail.alarm.AlarmSelectionState import dev.icerock.moko.permissions.Permission +import dev.icerock.moko.permissions.notifications.REMOTE_NOTIFICATION import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3a59be94..80906eb37 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -125,6 +125,7 @@ sqldelight_coroutines = { module = "app.cash.sqldelight:coroutines-extensions", moko_mvvm_core = { module = "dev.icerock.moko:mvvm-core", version.ref = "moko" } moko_mvvm_compose = { module = "dev.icerock.moko:mvvm-flow-compose", version.ref = "moko" } moko_permissions_compose = { module = "dev.icerock.moko:permissions-compose", version.ref = "moko_permissions" } +moko_permissions_notifications = { module = "dev.icerock.moko:permissions-notifications", version.ref = "moko_permissions" } # Test test_junit = { module = "junit:junit", version.ref = "test_junit" } diff --git a/ios-app/alkaa/Info.plist b/ios-app/alkaa/Info.plist index fcea243a3..f4abbe013 100644 --- a/ios-app/alkaa/Info.plist +++ b/ios-app/alkaa/Info.plist @@ -9,14 +9,6 @@ en-US pt-BR - NSLocationWhenInUseUsageDescription - Permission used by third-party libraries - NSBluetoothAlwaysUsageDescription - Permission used by third-party libraries - NSContactsUsageDescription - Permission used by third-party libraries - NSMotionUsageDescription - Permission used by third-party libraries CADisableMinimumFrameDurationOnPhone diff --git a/libraries/permission/build.gradle.kts b/libraries/permission/build.gradle.kts index 5c7a094b0..14c099262 100644 --- a/libraries/permission/build.gradle.kts +++ b/libraries/permission/build.gradle.kts @@ -13,6 +13,7 @@ kotlin { commonMain.dependencies { implementation(libs.koin.core) api(libs.moko.permissions.compose) + api(libs.moko.permissions.notifications) } } } From e7bdf4d4c9b2925261698320d910d35ffe2e2df9 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sun, 16 Feb 2025 16:49:06 -0500 Subject: [PATCH 05/39] =?UTF-8?q?=F0=9F=8C=90=20Remove=20custom=20browser?= =?UTF-8?q?=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The custom BrowserProvider was removed in favor of the LocalUriHandler API, that is available in Compose. The places that used the custom browser were updated to use the new API. --- .../preference/di/PlatformPreferenceModule.kt | 3 --- .../provider/AndroidBrowserProvider.kt | 16 ---------------- .../com/escodro/preference/presentation/About.kt | 10 ++++------ .../preference/presentation/PreferenceItem.kt | 11 +++++------ .../preference/provider/BrowserProvider.kt | 14 -------------- .../preference/di/PlatformPreferenceModule.kt | 3 --- .../preference/provider/IosBrowserProvider.kt | 12 ------------ 7 files changed, 9 insertions(+), 60 deletions(-) delete mode 100644 features/preference/src/androidMain/kotlin/com/escodro/preference/provider/AndroidBrowserProvider.kt delete mode 100644 features/preference/src/commonMain/kotlin/com/escodro/preference/provider/BrowserProvider.kt delete mode 100644 features/preference/src/iosMain/kotlin/com/escodro/preference/provider/IosBrowserProvider.kt diff --git a/features/preference/src/androidMain/kotlin/com/escodro/preference/di/PlatformPreferenceModule.kt b/features/preference/src/androidMain/kotlin/com/escodro/preference/di/PlatformPreferenceModule.kt index 4a9670b0c..7078badcd 100644 --- a/features/preference/src/androidMain/kotlin/com/escodro/preference/di/PlatformPreferenceModule.kt +++ b/features/preference/src/androidMain/kotlin/com/escodro/preference/di/PlatformPreferenceModule.kt @@ -1,17 +1,14 @@ package com.escodro.preference.di import com.escodro.preference.provider.AndroidAppInfoProvider -import com.escodro.preference.provider.AndroidBrowserProvider import com.escodro.preference.provider.AndroidTrackerProvider import com.escodro.preference.provider.AppInfoProvider -import com.escodro.preference.provider.BrowserProvider import com.escodro.preference.provider.TrackerProvider import org.koin.core.module.dsl.factoryOf import org.koin.dsl.bind import org.koin.dsl.module actual val platformPreferenceModule = module { - factoryOf(::AndroidBrowserProvider) bind BrowserProvider::class factoryOf(::AndroidAppInfoProvider) bind AppInfoProvider::class factoryOf(::AndroidTrackerProvider) bind TrackerProvider::class } diff --git a/features/preference/src/androidMain/kotlin/com/escodro/preference/provider/AndroidBrowserProvider.kt b/features/preference/src/androidMain/kotlin/com/escodro/preference/provider/AndroidBrowserProvider.kt deleted file mode 100644 index cbdae2a69..000000000 --- a/features/preference/src/androidMain/kotlin/com/escodro/preference/provider/AndroidBrowserProvider.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.escodro.preference.provider - -import android.content.Context -import android.content.Intent -import androidx.core.net.toUri - -internal class AndroidBrowserProvider(private val context: Context) : BrowserProvider { - - override fun openUrl(url: String) { - with(Intent(Intent.ACTION_VIEW)) { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - data = url.toUri() - context.startActivity(this) - } - } -} diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt index 7b4c9b8d1..afa7afb81 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt @@ -27,17 +27,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.escodro.designsystem.components.AlkaaToolbar -import com.escodro.preference.provider.BrowserProvider import com.escodro.resources.Res import com.escodro.resources.about_button_project import com.escodro.resources.about_cd_github import com.escodro.resources.about_description import com.escodro.resources.about_title import org.jetbrains.compose.resources.stringResource -import org.koin.compose.koinInject /** * Alkaa about screen. @@ -95,9 +94,8 @@ private fun ContentHeader() { } @Composable -private fun ContentCallToAction( - browserProvider: BrowserProvider = koinInject(), -) { +private fun ContentCallToAction() { + val uriHandler = LocalUriHandler.current Box( contentAlignment = Alignment.Center, modifier = Modifier @@ -105,7 +103,7 @@ private fun ContentCallToAction( .padding(vertical = 16.dp), ) { Button(onClick = { - browserProvider.openUrl(ProjectUrl) + uriHandler.openUri(ProjectUrl) }) { Icon( imageVector = Icons.Default.Person, diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/PreferenceItem.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/PreferenceItem.kt index 8faf336b3..946aaa94b 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/PreferenceItem.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/PreferenceItem.kt @@ -19,11 +19,11 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.escodro.preference.model.AppThemeOptions import com.escodro.preference.provider.AppInfoProvider -import com.escodro.preference.provider.BrowserProvider import com.escodro.resources.Res import com.escodro.resources.preference_title_about import com.escodro.resources.preference_title_app_theme @@ -127,16 +127,15 @@ internal fun OpenSourceLibraryItem(onOpenSourceClick: () -> Unit) { @Composable @Suppress("MagicNumber") -internal fun VersionItem( - browserProvider: BrowserProvider = koinInject(), - appInfoProvider: AppInfoProvider = koinInject(), -) { +internal fun VersionItem(appInfoProvider: AppInfoProvider = koinInject()) { val title = stringResource(Res.string.preference_title_version) val version = appInfoProvider.getAppVersion() var numberOfClicks by remember { mutableIntStateOf(0) } + val uriHandler = LocalUriHandler.current + val onClick = { if (++numberOfClicks == 7) { - browserProvider.openUrl(EasterEggUrl) + uriHandler.openUri(EasterEggUrl) } } diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/provider/BrowserProvider.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/provider/BrowserProvider.kt deleted file mode 100644 index 723eb7757..000000000 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/provider/BrowserProvider.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.escodro.preference.provider - -/** - * Provides the browser provider on each platform. - */ -internal interface BrowserProvider { - - /** - * Opens the given [url] on the browser. - * - * @param url the url to be opened - */ - fun openUrl(url: String) -} diff --git a/features/preference/src/iosMain/kotlin/com/escodro/preference/di/PlatformPreferenceModule.kt b/features/preference/src/iosMain/kotlin/com/escodro/preference/di/PlatformPreferenceModule.kt index 24d9aaafd..2c5fc0a73 100644 --- a/features/preference/src/iosMain/kotlin/com/escodro/preference/di/PlatformPreferenceModule.kt +++ b/features/preference/src/iosMain/kotlin/com/escodro/preference/di/PlatformPreferenceModule.kt @@ -1,9 +1,7 @@ package com.escodro.preference.di import com.escodro.preference.provider.AppInfoProvider -import com.escodro.preference.provider.BrowserProvider import com.escodro.preference.provider.IosAppInfoProvider -import com.escodro.preference.provider.IosBrowserProvider import com.escodro.preference.provider.IosTrackerProvider import com.escodro.preference.provider.TrackerProvider import org.koin.core.module.dsl.factoryOf @@ -11,7 +9,6 @@ import org.koin.dsl.bind import org.koin.dsl.module actual val platformPreferenceModule = module { - factoryOf(::IosBrowserProvider) bind BrowserProvider::class factoryOf(::IosAppInfoProvider) bind AppInfoProvider::class factoryOf(::IosTrackerProvider) bind TrackerProvider::class } diff --git a/features/preference/src/iosMain/kotlin/com/escodro/preference/provider/IosBrowserProvider.kt b/features/preference/src/iosMain/kotlin/com/escodro/preference/provider/IosBrowserProvider.kt deleted file mode 100644 index 2debdf9ce..000000000 --- a/features/preference/src/iosMain/kotlin/com/escodro/preference/provider/IosBrowserProvider.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.escodro.preference.provider - -import platform.Foundation.NSURL -import platform.UIKit.UIApplication - -internal class IosBrowserProvider : BrowserProvider { - - override fun openUrl(url: String) { - val nsurl = NSURL.URLWithString(url) ?: return - UIApplication.sharedApplication.openURL(nsurl) - } -} From 9f3b1eea87bd8efe3c55c13c0953b2e4c8e78484 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 19:35:23 +0000 Subject: [PATCH 06/39] fix(deps): update aboutlibraries to v11.6.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80906eb37..f773805d6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ detekt = "1.23.7" composerules = "0.4.22" # License -aboutlibraries = "11.6.2" +aboutlibraries = "11.6.3" [libraries] # Project From f1d15219c96765b28c182e5e8cbf24a550255db0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:03:51 +0000 Subject: [PATCH 07/39] chore(deps): update dependency com.autonomousapps.dependency-analysis to v2.10.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f773805d6..4d44834cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ android_sdk_target = "34" android_sdk_min = "24" # Plugins -dependencyanalysis = "2.8.2" +dependencyanalysis = "2.10.0" # General dependencies logging = "3.0.5" From 47ea563c867a65b757966bad3e21e97b42455c88 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:02:08 +0000 Subject: [PATCH 08/39] fix(deps): update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.8 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d44834cf..c41a07c07 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -63,7 +63,7 @@ test_manifest = "1.7.8" # Quality ktlint = "1.5.0" -detekt = "1.23.7" +detekt = "1.23.8" composerules = "0.4.22" # License From f0db85591699c268874b2a1af160477a0460a050 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 01:58:54 +0000 Subject: [PATCH 09/39] chore(deps): update dependency com.autonomousapps.dependency-analysis to v2.10.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c41a07c07..69bdce02c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ android_sdk_target = "34" android_sdk_min = "24" # Plugins -dependencyanalysis = "2.10.0" +dependencyanalysis = "2.10.1" # General dependencies logging = "3.0.5" From 33ace0988344e33794fecb6500b09e81525dad24 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 12:39:02 +0000 Subject: [PATCH 10/39] chore(deps): update dependency gradle to v8.13 --- gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 43705 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9530d66f5e68d973ea569d8e19de379189..9bbc975c742b298b441bfb90dbc124400a3751b9 100644 GIT binary patch delta 34744 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 delta 34727 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�{ zSCu|?z-R)71@L#eM!y^Z6p;ZjzlW@gZzHJC3~O?Pk5QEa0q(aFy!-~pFZ%vBM{a0B zOfAZFmYc{!vg!PSF@l2U zJK`=N@CTmAO4Wuqv6k{SNl?~rs-CcW0VFIdAj^B2Wacs>M@3N&63=c06V6Rf2sR|QLucLaU zKEq5=F9zA=+3ZT|OlY$lIrFmvTV4H!iv+MxhtKJ%j}wlD3qAoT@g^}Cw`#0dsQnXX zETbS9p{IGl{fkz7ld(7^$~HEkkh7pv3NYi8<1qwOw!a|xaQ$TntGU7;01Z4?b9D8N zBh&aOYgatY!f;X<$(oO>v=8iOcEG%aUvS8Uu1du6!YK*G&VLOXlHRCKu=FF(IkNo_ z!128k!z=B?9(@872S5v{*=6WjNH3gAJAUYkC%^7Y;H4r>$kZZC%?&3E-qa#4n-YG$ z{5tlV`bCK=X~Idzr7&v8p)y!whKx;pP;V!X^4&igR1g*2j}8HyVC+>KqbPFthf}+i z5*V2^NBvmwfWIU)3;IBGEwFtYFWVWUoB2RyvL7S*E#d%FT_ytxM895Q4V_PCQh+>< zlu~L{SuQcQ?il+AeFdE87H!P8>HgIJjkGW8@`{o5wNd6uVn=dNX5$aDi14$pTSR=` z!YTmifM=Cy`Z=%xX-u&9>1bJBw3nKr0@mO&YfAp~^V^fzVJyvwMY(hM5 z=T^FaQL~&c{7fIT@FE@vI;GbS=Go0=v=3x<1AaB@b>U z;-hwvu#U||CUj!>9G3YgO6yQX+H)L6*ozXXaV=U_b`_DQWq#`f$?cZ;??y9(AcTLq zHrc9U_$w&NRKgWZ>e};_T#tf-g1TX#Ttj{JjKjCJqlf63U8$=~02ty9Nn3p2WX;CqqYS% zz5QZEArIj!d6Y0VI^JFWKudu=NFUPF=6TxRR|reQB5_2vIn)qBV}S3;MX1}04E3Mt z#5d$zK8z>OW^i7tXPB6e%UCqcK(le)>M}pUp6H17YHZ$`4urRAwERt6^`Bj>zwymc z6H+f|4zhQjlg1Gy%93Sw`uMScxrA;vQE~ta!zM?jz@&c;IxYkrPHXB+h4)S0@SIgF zdm{UTZqxJaxzBR!!`71;K*uco18U~X>AK&Pu-C&`R?B-Aj0=_$cxPzn{MlJK>ywJq zsw-Yj{^>7%vDCYw^iw(od$~o-Pz6ks8aQ}A1JFWnE@Ez_SYh@cOMFVY`?D$Y&Z~a1 zd>zg|c6+o8_xSfEUIvTsdiN&WOe=n|xS;8X;CYLvf)|=u($YtOu_6J z0tW_ukuKXj2f=f}eva;=T4k7`&zTqf{?>lGm&{Fe_;9R2b^^i}Krru0>ta|4^_A$H z7DO?PFho!p4A2C|$W~JYbWN&eW(4R;;Tmhz zkr;EbZ4D?Birca@{afZpp_|p2YAInGJ`1Fkz7A$droV0#{h=lZdX+xO4B%I?B_3ac z=7FCkf`P*_R`SaCnBPG1Jd|Abx!brVL zIt?Rv1@qnIGKpG7W-M54@Oi;BujL}Xdacfmc_9q?u&4#P2hPg`({??ZOOjRFnps_D z-f(IqU)UUW`f&U}`A@568jBEz<~CX~Yv+1et@-+dsV3RVrNTx?H9ht?VAAS0D1{G? zJbr4_B_Tqy_Ag;Xppzr)KXQ9QX}21eoMW|m_{|BBHJ*=OjhvNq(4HgLp`u-X3tw>X z9A?^?H5zIU4r9K*QM+{?cdUL9B5b=rk!&F@Nffz-w_pG9&x+7;!Am0;Llsa02xfYC z*PtggCwO@a;vLXCgarLHOaCqh;)QBGzd)|oeVtn=&wvyz)rOR3B)bLn=ZqpwZHq0G z#6YvZtco3reVEzgsfMR6A16B&XJA|n?MuIu8bp_){SA_{zu;H?8${rR&r^T3v9C(nb5F3yeC zBCfU1>1a`bLUbS{A0x;?CCtvBD58$7u3>y2A_P9vigNVLI2|Lin+b~C-EytjMOHW0NTui}pkxXdFdIJ$-J+Bm$%CN%mac~u zc65u)RMsVt!-|8Ysv6BvqDBlFKElp~B6L!lpd@XpeV9f#ZPtB*A?b!2cQ>(0KpkD3 zcX2g{WebJL!6EmdE>s!+V>?WUff2Qb1G0)SgHlNwmhKjxqoM~UZ>S=G#3}dZqbOgm zLQr$%IH~rG-VibZjQxA+wx_MOF@JC7m(z5WFp@?e-&dnA^W!f5(1q_mx7SHG&7Mjz zJ*FkzBLiO~YXM}_WN$-^LB=)#9j0}Ig(60{oTJ7L{`hY&|LX}pO&lXsa+ZJY)@FOggOhohsSKci~64T#~a*U>?#ib&8;moQD4mX2U+S(Fg|)$9R86W zITbI3PGBmng{xAMx7@wkfPyHgTBnY--U-MN(8g4;hg*?%-H-2y9+fMsROmUruu~DJ zD`y+zHt;&kEmb0pX<5f>5axt7b!mHhGZrk)cPJl8fFV}4Hof{DHc?nmlNe4OZlh%Hw~gDORC9fFH@ z(dp|iOIbEM2+*ogN5G5IIj5N6dcX2{rbl=|y=_lReUu(wdD=vfPY1!pN@X;H)!7M& zsVSTH?G;8EjqWqJgt8F#raa9{%Ig46>|d7k@)*edY9u$q-2MD_g(YtesUb(fF@ zeIca^`q$v%I*l@1*pSA^WwV15>IOc#+Fmv`%pKtg3<1=cn#Ja|#i_eqW9ZRn2w?3Zu_&o>0hrKEWdq=wCF&fL1pI33H z5NrC$5!#iQpC~h3&=-FwKV0nX1y6cWqW7`fBi39 zRr%M}*B_mXH{5;YJwIOwK9T9bU^f*OUt#~R;VnR}qpl2)y`p76Dk90bpUnmP%jt$sr^*lRURZhg{Jc|t% zzJ@`+8sVJPXQ1iJ<*|KHnVaNh6Bw9w7(H5d@A2z)pFDaQHfA+~;ft*Wl5TXgXt$X+ zw>HuHuNiPuH}l);i?tm23b}z`d*)Fc#9aSTR0**x64KPFxH=waD^aF`<3*U+;u(Jl z%Vml|ibUgNPW@Mu(3F&xqqX`Ywa;f)vz@_@ai=KchFb+T#v=)>bVeCp(|;s8%R{-yG(vI#MB|PpTf%;Q_dytxihYgUEEp*4UnBD2i zFzwhlAsbs^rvyOn1@$Y4a#xL*#mfe*-%9pKM;rMxBrQ{x6g=Z)-ac6r2QHFaIB3Cb z)MlIq>|a&HnWt;JF7aNioc_56#kOM7`*3HQOh2zj587o#jVvMmd0^Lq^}+G*kE4L@ zyr1bonUrLt{25*}164@vq#vyAHWXa=#coq+BP`G?NvJ{D6iI(?WK_#=?Sghj z1PAobWSn&T1JN2+aDKWLzLa-vkU}op+rSMu-^54o|YB$BNlXsc4)Pk+N;1Zjv_2G@*gdMul2v zus9!wq9-nM_j*C2j*4}T#EOpQH+mG;>6M45k1Bv!l)vdjfmgsSe9%ze*37SC0>9_L zi$J!Ziite+mT#sPW;8{9EdmpRcM_V2yctTOVr}V45Ya@X%iVpnLr%`<6JxcpQZJW7 z8cdPFktXB1WhRl~Hl4PUPw4E0+n*{!yDCO9mjal(#n-SeE6ATb`3BWpmcOoQtW0YC&i_4DFt9eMt#<$YtDl1dXA!$_EIQN?X#w1#3P}!YVg2_+D)GMjl zY@_EZ_ZKP?D)_w?>J6RZnB*Q7Ruv~$QHEOp7abg-XyAe)|FAORoics58~_N@dE!`8kvn*VMyv=fg8F zE;Y1gK-hU9#R`_&5n`$v&+@j=#2b-LIZsY&v=}NAOjfOB3*&2UItP}{OqgRpGh>_f zh%mJf#U&@U;;T#cyP}$M2?X^}$+%Xb$hdUMG3A`>ty6>%4yuP<(Yi8VcxH+@{t9(T zEf55zdju@GID-2&%(4Va<|Ra3khy_F5iqDnK(rPsYx`73WPueFWRJV)QFt_0MR4ew z^AAwRM+u8@ln#u7JFYkT)O+ zi#|KR&In+^((C^Qz6W~{byGrm-eEQBwWk;Gru$Vq&12PTBnehngdy#zSGdTlw| zntnZVw0Zw8@x6+gX%7C`9GLL`vpHbla6TX+B7XSrfgEy0hYHbGenBTju?E1^# zcPx@a{i?zW3ISa;V@%Kjgr2)Vx3UHv;v0j#v5i!do{bld!wDqWoiXLi;bP20NC_Q1 zWmLa5QI~_)A`d}#*aQ+SfANbQB7Qd!Ncl(>6 zheiX141UI3v(dtiSKg*zR;+|a*Uv_OU@_I@u$Sw%+tp%rqDxg~Va^*|OD%zXAYe6! z!Osuw69pNHQ-?@qEDa7bt^Ga?Xa(5g6(KJGSSDy#r$D2V;~$a?q6O+}b4^#6wsf5E zX_GK0Km%Z@vtZr~zNs08B zzlMH4(M*)#G5 zynvFiw~srA#@cLNhHk`!r@!W}8-+5UBM7C2P^oZ%kc0uzbTp>FHRO=xYa=v)0aQul z9UgNxrY#bF^%AFxsI;{sv#0ekRc8}5bc+e-tghcK-OU0FGl`O!q9lk-bQK3kz*s7? zV*U~Q9=~-fem_OJizGL{$4*=a7|@ZKwLY%#p@2?FP3Q>15nTl#b(ZW{k6q`Nx zOMonpItf;aZ4(|66znCH7E27N)R9I&GsIJ z*ClS8kTkcOvZ{S>Fv|`^GkxEX=rkW1(MQX6IyC;Za75_)p3!=|BF|6pLRsYUq@}YIj4k#cwM<(2dKCeZZpd6cJ$fz6 zXU8ca+ou~;k@S379zHDD8S5)O*BT7~{)Dj3LCoshK9dt=*UEKo$P_!yxozT=ZtBkj zev^`G~ zc4AoF3d|9i#^@>JywzuSvW7krJ{v(4IX&@ZU5})Jy)F_p647?_s=B2@mHHAWI5l=- znNFit0x5-AIV}8zv2z;Y-K9McGGqK{hU0@PjRaEJG*_X4Jo*Ua=DamQ8b7f09*Mazbhhn6LBj%&=C`Zw8uz@XoMbA z%j)N=G34Q-&zQal!IQE=*PWyC%Nzbkc?SQz^J9l> z3}_mkctbvtd6Vvr=Tx5dQ|k=lg-=zHk76OjP=g9IPH_%tWed^LXiY9Cazf??c$snr zz!4}Hl4G4@_xpkYJf2FXoKOO9-6J)oiWYVXuSJAY&Q`aFnV)5L@nU~x9O9VuEbZmm zRJHYpRyw?}bQVa47oYcRa)$0@{Whq+Eszd#|A;H146&zmxR5#?^3=Qdiij=KX-Bvd zk&plq0|^#&B~AjImXrDvvJ40$v(^a!JSp>w3$@6tFc)7&spiek=YVmKkS2(%uo;S; zqBCrWkh+zGsP=MQ_NEL>&43-zSnE7k>kbEB)jJWqRV5}k>J?*Rcn)jx=c`6*MZ~|i z%~^le&(UQK^+n_>?xxUQts<>aPR-TgOJSE6Uvk5ZUkP+>VveCD#mghIG(nOynL#Rs z2$vVgxk2{9-OsO=D`|Z%@x3w)&CjCgeKN0P_V|BE-c%IL`c-nXVk9#S-YNj3*P!-C z^7XvFA|Fc zQxCIu-q?|)UMe%sa3wKx=4brU5@->gWRLT4CltHUIy;}a|KrUJ{a?72odi_$Jtv~g zkQWC&u|Ui#HMR{#IS~nXxMkhhGSf zY@Od4)>#^qTHlZOA6ih(()g<+OnN3wb6{Q^(N3|JFQ>wk@M>uhX) zr)h?8eW=WL#|vUm?PV9~lwWnXh-FzzJ%!x>#?s)dgZwur=+ie)NL%H#f~c%;e2_O? ztRDfj%ldcOwjk(ny5_GYpz}QMZ&YY${hM|O2AyZWre5QzFI62O!>~tkqcDdtBY{-$ zuP(XeSh@3Xk*0o^Wa)qAsTKNxZe}ik_%)PtKt<$f>wWvxMo*99^R)3&;*5cJd|r=q^}Qw~=ZGkr7Dg^@4b4T-b$ zv#R2Xe!$2km%(4C))AfZ26hixuAF}-+f zZwfDSoMo+1_8Bu$7xPtlaoSMSxTLFO1~#1+>uc(Djj`l$TpKz(SF{%R8g%NC7!}{IaPsNc}&S&M`WZu4&tu*tTukwv8*!#C9^# z72CG$WMbR4ZQGgo=6>GqNB3UctM{K?)xCF}Rdo~rsc4{MqGT*X7Wi1f9D7k%cwP1a?U&RIrc`PKXV&fRKgI#_d$X(&SXS1O&!lRovJGQJQVg60S*AF9wDZ zh9=X$yV0h)E%*z&CuydVyRSQ+JH9@TQ=dpevf`7)2Bn*IUCx&ilfbHu<}m{SoElh7 z39m})DpJWpAR!Qp@x3%)%4JbzWB4LPxVLQRSboj0EXO)iCbQ->>+)1T{T~oy%}-k zZPiD;=v1*g?z+0TArLF-QXVcw-NDyEHfrSgjtgkt>ep=3P%Q6WnvrJt z+4RwtdR4Q#RUS7xS~!Qbs=E;lje z53Oy>LXWHQ$2v+95NE2^FeUsgp1y4FyvUw1VadDrg*G_B4otGbMYIlWq>so@%yJ!C zV+>DAk}AXSYO|>TXO$oecP3UZixgcI-#ccF znJq7up8Zjx1AN0)D-mL!udb@{XsbvCrCnAgur+f+WxIfw{$K!o4 zfn|*egR+@Cqfbd)SeHLedNl(erm}_}Clq=82-p7cA`8%vq@&iJlk<}*b;&T@mm@wX z}1cA((mK@yos zPW0ZW@JX#qtMNijTe@pH1gG4`^<{AR@h;s(T} z&3#(~u$Qi#%j!zW{ss#Xsm|DQOrmKNB0cK9N~^$rZJLyDEKoClR=V$R;aujtgT#1b zA`U4#ht`VKoHWuito?@~br1x@B1L^j>cuo=exM!L_g$Gz0SpZ^`C+o-yaA}LPlf0= z^n~1R7J(vVSULvS{$R8709Q#R@ZbWBjZyY(AbHaC(7|(oHtzZ@NbtoHn;_g=+H3fa zy!pe)r}Lf|tftQ|FMWp`rny9HZ;N&8jH3-LHf6@ zM&!|x^O%ZcPJiq#EK4mpID>Rd469b;u>zA+kvrUva9OQIDXPl_*T6IGn29GAYKQ0n zASA;!l#^KpqRw`sb%#}-2}Ud`ZK&<)htt;RIog2CA2(DI+sP*f^;yl%Jzz6%{0}^a#h=NyKLgPR? z+h)#g+PQn_^B*+snviZU(joHWllOKpV9D$p5IwQbsoi6pC_`)m%$bm~s>3~@oHT|MFt~;^&e$k z`!AZ@c$^%MzW3|Jt;kr?yNKC`4g;qphv-mowYqO~qxIDHG&T*1Il;sp@iK|H~; zRY8%8d5`6`s8oac%2s^AFKN^&{3cN##QttYZ`4w%O1kG)vS3r_nko@(3WSWY^hy%k zD_xZkb0hmkTBJdfu$mY-P*DN?TlRxM-eP1OB3FiJK5ogaE%S@t)Zzn*d&`8NQU6AL zC9qU0aDA(=vpOu~8PPvMOGiOGcbw0;i&OIZa_^2(khD z;&117LsI_yz=<&pOSpyG0=nv1z6nB$uqp6DxHM4~*{6ytIT39}>Z<;BowyqFU@THt z9tvb``MojCN=M7LPJs?9k>}02!$N}>-Hdf5sj+7zPsGcEpJ72v5=@DHxVbShM znTCaXY66l$r(TQRo{5JpXcn1GZ4$yFyu=I%t%@xcR3pUKP%~9_4y2j%Q(-)PkDfn} z9I;eUk*#9=IplZ{KjMiWV(J5dk%FI*g!Mq0g2h}Kb^c8wfG~@54Ml|sRB_zCI<@{6 z^>GrT2@cGf?mzHC4F8I^S9r33+|on(dnh|1Z>%)RxVYT~j~E*AoAP*jexWIP76myS zPmxHAcOLo4+KFvX7leBb75ClA;yi&nJL{!SU3@ zWMvA{qx5Pu{sRs@9^q`F3_ray9*Q&n76E5u$F_G0Tl}P{sn+HS)^78+pUqFXayKO{ zi^~-OJkHkEj&_t9g1Y0<`H^--_8B+x!zqT9=#17`5WUA@RUk-mPwZ;c+8RhB+N`=K znJs*ymvdg07$&iKn$G*Mk6>^D1*zhr9ipPUJ%R8Yk{s78rc=2jq zx?!bk{FtF%6OeF@OlMxwiOa{3JZqSunUzIK$Krxk3j28$=JhtBUVAPyC$e(tOs@2&>aIiai+vP@s~9CD!K+B*cxuJH5{ZoroEdkOb07;B!(&?FM&tYiDzMEi^#Kvu)$>mUMf_&sIXt9V z1`|{6PuR}`LE+?M@z!%&B1y|M_RaF73@U??hm`07>sJ^Y!2lLnd(8Vpp>y1ny1lr3 zl!y`Wp!J+)z{ok;P0$-LP(J+_fL&p*f0=;J+-ts3-7_(rS04#pN+)SQz)n%tOxR6_ z@iS9s7}z{TeV+AZUSI^TvB)a<)51kpw?}19ciIMhgxJi+fk$dzsUIxLVQ}Nw6>zz% zYtr38Z538+YKBWeW51rNm{Tpg2qKiX&!^s#!ve?C(NY6ft*#v{M7+r!kFvwni9Vg9 zVE>1ImnPXi@nY&lD&bwEzxTI{dNtF18pL$JC~#UVZdYp;{nAd(+?7ql2-I0p0a3h^ zdE7VU7KJ)trJ-z)KsCRt^QH%e#W!F~rPh@w4+*$@ zK4)>+_gDsG){RQP2XFWefCz@LxK4qr#%x=WmPy&Qi9cIKa_7gh__E4y=^U1@#vNfA=^ut28X2_ieyr<^WqKZ6Z-Or8MH|Ad<`?oNVuOc^D;a300H_ zM@89Pv5h{>T$*iPbD?^mIOFe&5u_Bf2CQ{5|AFdS+Fwi*XSv_QuaOXm*g$E@V6`8E zQRKWE^)Z_$Y0gO|a~q&cE+vcV=jv9uS%8|>#SnVFD4{g@06WNT*HBsw>2!tC0{d{{ z-?m)$6BB^p0Jsu~0e@^&+QoxKB>XGk((rAyZ?!zC_Y&)X*aR~{dd)P4=tBS}&bgS2 z{qy^PL8LkzJ@}LlCE)1?0?Rcsi(8&_kltfWR6M$DM zB@k7TLP~t7P?uK;Ts)*HwZe_wZDjbBZM%!6b?Jhxe7&{7sfsC;9!MX@l+!aDwGefQ z4x^TY#)Apr3tC6_!dw?x(%AL$?5VUr|4VvE0UoX+_onVuhyG zjno6xQ`GYfpa&yn`;1$$&NDY>HXLD&54al2@3A?CO|q4u_Avv9^NpXV^|y@IoDy42y31Z)~eiGpE6 zjFQWawJp?DvP0va!#N^er>_g=QN4?!$QgS^+?fbZUO$e-pB_^&i#<6xi*}@zikhr) zQ3p!O-n4OUat{Ysi^*BT_O2f8jyx#;l8S9XRMCoMZ2A)_ zX({EoS{qBU0kjhm%{)Y@gbA}dPEho2-^nP_{xyxl3R{(C!oi@~ily18z0RaLa0~`Q z-}?ov&mj*bb++L+Cn&la1{QW6ioeY&-ik0^fbt>FeFp7$E%vk?b`~WsQnvbzyglt2 z9`}pj;QLZOF2GfJW`1Ani=s|17tLg$8U+`!R+s>XANYrUg=l>KXV@4VJI=(f0lM4q zc{QF7gEfqt;%le{C3*5Z;l{WC zFSAqZwN$9H)7C|NkiQGy?ue@E(A}7Xg?|NcL2!wKV2fX9dAtshHJ||p-F=%=!ny8q z6#06TOF*fvSQIa|E4OQ!zt_m$j8YEAXLb#*=)p7dhKLDe#O1>ypGw~Mhuiss4SE&o zUCOJU9zDRJ%X0NAEI1iD47H_vlSGZkF~C$89(cGGOkm&MeNlaq=G0Z^LGoC#&+(5; zaLHJmE~eLwe)P>Soonm@y#9COv=j>${%>Y)XCS}#)W(vgsSVQX`2E(M^D$y3#n~@U zgV@DGaFc@HzP4;aOZH2b_Z$V?;5?hCMg* zn!6cCC{y}g^m+AoL?$;eAC=f(GWM_EJYNcPYf@{mDE%^ugN=T0ugCc2Ib$OHbSS~)R(7Omi zjZ9k3U(d1-{M$k<#<4`~+j1kbgN}?&yxq;C&cE~NugdUGNRR`qr}^`}2t-ziw}9Yu zND&z4NgN_teN~?NfvUpDyi>c_B^0D$$U%w_9IM8HxQLYy){J#zv$J|XC2k3T=4g!TR3r2+)_P(#EJsgpZU#ejJ820y9k*w+P@sqnB zl9o~obFSN-5jU6z9D=9cynbWie^HJCnF-Ek_hYH71W5_lcLsNLo|gKJBcNoqk5c#` ze{rg+LtS})^(X{gJxq+Am1Jg{hJ6adCBk8!+}{d>I_;u1kC3In1Oy{5Hv>zNHJZs5 znjAml*}FNZQo=Ul=BGBKuJg#6S6ZrlZyojk7hV6B@O&_H#+`Ni^H}s&=v1+EevijAm=O*FaVtKKpajjc} ztaO=b1DMn~BYxd*1Ljzw4}l3A@`qiyNuq=mV%qB(#Sat#fi05rT^EFLO~bNLgjSc> zSJeJCu>K0517vo(tmJk=ys?J>M|?&{ev!nS5H~cObS#1rSXcN(j8<2c>5`D6w2tf7 zjkvK{8I{la@AP+{l|PZ5ymZ+vIZ)x*a@lgzr?3`tKDAD@YKBNf+PeRun(}CTCE(QK$%Jyv^`vksei?l5pL8gQ{6s0E?fw#I?&W!G9 z+C)pZbxWvq8L3$`GAe}p$97nO+37R48}bxo#dEr&Qg2J#ZMnsBo=g#@IeASh%rv$3 zCyobcB()INWZIHZD`1NqVUEe;JpLx>!$#$~`lfTHjZNvIt*&KmP29<5qHD)>(a~>x zDT_5fVT~3K%Ybc3xNBC1#@T$N^+~ISZ6!Z%293?xQi>N0^`8#KfX@*0`rA@o@8FAT zsB`&GEUOCN_|)~=lHXT#bL%f2XZWAqP55N5u%n`YbLctRQH>0A*QR;vQFGqagnY+W1#k`J)!VJdJRaXokyH%~~(F{OUSN8mX&?MrQyK$stRrJN_8j?Wp zkvR4O{4Z^Vqxx%u2m=IUj^=*~`lcNV5Y9)}4C60QCd=D9OJJjRd!f6-KB(4iLqL0d z06RKXrX;z+KDpkwUBP~_lcJsC)qGnR83P3c9A(LFOs=@F++QC+{gdCcPuUTcIvlZ| z1hzapkd$@yJ+ayMyfQFU1*rdhojeGzLl{LMmVJLfqNj@w~3XBub!DJCFknUoW~z8qjLV2$^@+>HX1 zzkSZ4A3OtiiMH9G)F{x8-`pxn7O@+>p8bL7A}3@y3{7A@M8Vy*CAVFWIF!T1DH%dJu5FlvnwyLF0#cSdT1$M6# zZ18qzTQfAt9;sl^A2aK%_~@pCg>_Qp()DFxmpa6s=1SZ4*=uzdMYCjqo;X(5oMhv{ z(dB(zEBvvp#a1pisvEaXUh>{EKF)%>rO~fl_8B-_Ime(8ne*WlnsG* z=ur;WDhz}R_=p6&Me__0Dnqa)Vm(Gjshb;d)FwR&H(;EMbdzAFeKFCT-Ig4E$-4aK zGi-#-;?EInxP?iXbRq=$>IBkhmhdo$FOD!Kejf)(j0kQ2kZL;=o?Rn5)dp>0x9TTa zCPh;SH*Hd8zFU~s1yV6Aqabc3g)G)YP&0~_iN4(1;c@Mm-(~T@_R?w9F6{(DUIimi zp3cI_mO`0P?HWD-gKBwij}GDE1U1oqsx#4xf_P&!$(ge3=p}rPpg(z7QtSLwVp%wr z)b0###i4ADrG59KZ8H5jrgmQYIGWL*j+|7cc$#s65id0@KZnq(3&wC@I#!RvrVJD` zc}=SdM#lo1wY7qQ?%8r4UAkOF5s^!cBg2nM=0e+U=;dHNa8Rk z6OSdR1P^6%75kui(xcdvAns#PwNEUe)W6QKvx++Gk|I@P=%B{I!M1%mN#BD~Z&~S> z$J6!HZEokW811c=}jB3iJ%ga)vN0pvV7DdI!MQ|gk(^k^%8^T$}3nBR>8|jLy4Kc zE=NuJDc;yGJK4Q)RVO0FMbi#2d?W{tqrvP2@CjY;agYympLu+8SM^1Bm^UyXv=)A) z$BGy?QAf}MC3Q9vaj5ue2ht+%CG->!2?Xo*aAjdD>+D7_N2BVDezDXJyMf0#@!V-l zodn=f$EwhwvPjP_`FNCTC?>YxIjNyQ{JA`OmQ^H@t*Ugyq^(rOx@Jb)%18SEeuX)K#ChVAWHY=G3=!Nw39B8L}Up9V)+ma4^A&pH?m z!ZxP?A|Ow92k*S%zgJf&B;)6NY_3^}60 zB^*Tq4Y^#YePB|#FBZNY8^FhrqL)yz@kIB=2}87#%Sz7pTM@ebhNF*?h-zOlGaGfv zZQ6P7qKX#@;EeeS%nI0kqiA2Vr6}63Y&%v5y0ML^&*z*~kj@ok`vxQmDwUd}iS^e} z-?Z%5Rm&l#PM70=N&Wo!2i0KZ&gRQpo@dtJqbT)p_hI@y$KO)UOh{V+3hcj2VhIFR)|`=Pg4tx(@};;bTtOsuNyB$QXe9pmHv*L z1ben*Fi>HnWoMC*FSQmeJ=SCE7~L=5TdT2brdx>Lpwa+1d|$6We068K6Wxxe&F!baQ|&s7pR zl$NXuC6`oi3J}9TYEA17G5kP5aP5fSaDISnI#xzANK&8QAygL9p|IKcF>Js?yRHxU zXvzf=6iuHcb=PWBZ^DVxxF3fDUpU6wevU*hwgyKVtY3u>XIdUCa0x^aO19CqYHPS9 zu`dYUXsTy$uB%DR^04ViJd4h7l#|9UlYmL0#XJR0%{SPhqaVrB&z{5U&dg+Rrx@9o zO385wN^)BuxZOicKQ)$`=k7N#;9Rnz+VF@5%Y`gGshFy8Hw5qg1W|DShA!yJt9nJq z$TD$(FaiuiWu6WUWb_!WUy*ZE@V4svwd&C@-1t~Z{HSQZ`B<(gJ*A@AOX3QZPVwMQNTn>MiKs)cfbC0;XP9g$wQ(ssw*!|cIBS)~BQVg{XNM;6Q z;Z4vGuyho7&kMD)b8KPy{I)E0CA9=YS*^)sySa<+o{t^_`#Wr&9lM#6YQ7DV>6?p(hnyN`!Gj7pUlUK!ybM`VhCQNEdRJw0Ukd^J@oN^+6;{FFz;7a!3hiE!Py)C;^8Cbt>|>vA@hw*yV9$+*+F}_|C^C{ z^$4FY6yp6QXa@b-Xbg5FDP(X<&GfJpd+IZhw5H3X1pyX`UgqephJAD<7@yKcmyak{ zBe-1l&h}3?t;+`H{Z5<-0A-Ed?nmf4oZn+6q=JKLD0`|9;b#lCP+P-NR`c8`gG}~o za_Wop;jix$On;U>r}s_Z#~q-fxnlbMCTVSaw6-|ETsY)HQi$+ZohweoYG;J!#MmYU zJ-&E}<7=c5?zK`~6X1y;X3s^0gnjdu`^z8PyA=m4zB2}%OVJ>2-(KV1!c_UG5tvz;-b<-P>67PMe-{!%S$+ge-~q#h{~r!iBIm0yR$+-JIM$&8J3`IN$zZby7XCwIYN&KX**xR?3#I`P@$25sP73{J~Fr{&VSx zWjo4(!WZY0!WRLG+&5_hs+36ennIRCGszV{g{c&nVv<_CY*JB76~&P_B3|dIkxj~o zswLyq+@`s3IgBXdfGL(JNd6+zp~TOG2=b5kop^*4-kRP~>$H7FNTn$aAkWn2(`%K@ zrFm>^ze(m-JNeWHOSG8y%D)sDXEXClyF~dn{9#!|`|qY&trq!g^80r!*MCE+{w?so ziMQ>7@&6_Yxnljhy1zm7fOt$qRr3GE8*nPAj(P{1Ed#RkgKMS8Kldx-Y36B97IYsk z|9}y6IW9i}gPJn_ITCs#0(+!0^=F_B17!!Ja0Fejsus9etsKjEH{|gRobo=RabqWx z+E&({i>_*%E@=1X|NH^2N9Z7gBRCL{zZm~NrH23ixJRLXwVMH>*4=hnF@c(Vhz6L? zfp{Y5=prJH88g|6MHz78O^o71L#>V^fpA29VW_j}65@zQ*^j4uK+%Uk_aBf(U@o9> zNJyvCe618gc(S4%qX--Jg9r=UYJd}3g)VM{2sg3JVv3zB=}QO#SbJNpmK#M~YdHii zU{sg3c`hw~d2=^L3ugw$bl$tWmJOz@l-DIhqBt!HD{X}KbwYy==H+zrbaN?|>TEYr z0CKrru|C>d!2)@Ga^_fEG(5+9tE4#&&R_0^_9d@-J|c81x}VBM4}h2AIy2OFiy9l) z2iDN_TbnQHnDsiZ1q<~HtUsOfO(hHZK(R8@n&|X&-gme5v8YW}j;=D)lv_A@`oA1+ zNUKZ`vXjqpP>7Wn$t?Ru;6+8)qSGP}KP5OAm_7UIg5B&VzSzLZ|8a+!1NZ5<@uMGk zC%5@!@%x4*mY3luwenb&Jx8X{=A`6&qZX+C^T;Z}lVq*`rMsN|JN}nXopeTxk#y!Q z1;nHgX~8#Wp%Il5CkUX>H2{TkrZ7rd*OxBTr?aAamEB~ISQMB2*=}#sQIjND1HPa_ z`VzU_VYSd?wZLZglgn%4^}vuEa|9P^noEhB(MO`zY_m{qND#(h`HJd6D$kG_kme5{oszd&i( zEO$uPV&<4Nk5pW9Y~0A>hUeCvz*EBZtGT4R@XC&cP9DRNGq&SM(;Fuyixh&|s@)*| z@R`oGyCdd^huhWJ8piCIg>D{fJaRF-E(BkVkmZr9$R)jZlgrWyD^K@hc1=v&CD8pe z|GW*rcuG~5uTj?g8(^WxCdG#oo4vAFn|A@Rd|ExPvW?j!sPofTRq+M|eN6jwD!arC z+^(8p%`i9gjQ87zSIaT_w`yIkE5IZBJF{Y3?WWGaHoew93sB1j*FTe;A{Yecfk@wu zpS8McksjKqHCMF1dFHK)V52~|0NiRI9G!n8tyZOz2fMkVdBpl=JIpar9_Zchau!WviRC`DxWD%D3h_317BbUl44j1a4&^ zGs$RKV+L}b>ga6jc(uQI1uWd|5+t!4_96Io%_HvJhrg2uY)acmo&SFF&mSd9q|{jTx^fJvbGU$-P~^aGpDRPn#1$1;sIRL24$V+`egtex zE0k}VA5-#zF0nBs%l&y#BhpJ~zUqR^xco=d$&7V*PH zZ=(514Nu-@FP;;Wg?->1LF)jYHi}1_6XDz?5r0lRq0^lXaH8k<3vAvt#)oP8Jqopn zrAsa?bw*t^03OdK3HpRM0`p{7XB=%X>0D6C*+UeG(3y##xz;tUM1{^fo^F%pfTlLd z#?dCv%;ETjo#!e$C)Lv`iA+?t?z5~zU%{cd-;DX>v_MGiYDW9< zxgX|zu<79r0gb4~B!MrWUytBX=pu9m7rpvVIlw0`O1cN41Fb?v&Z6_1mp2eH4{GvQB3CrHZWyrJ;VnXLHO@%E zN}Lo;kSiq2fzh`?=X#gM-#%8;q(d{1S4eY6v`^npV%ZZaTx~x^K8$(CSiZ=xP0G{T zc0(O^50=d&>c_p$N43*lVIrBX3n(=G{Ivvw*be|0`dVQ&l^=&sB&pxb7BL=}$~X|` ztZcSIzQG9LxDz1?LIBcJ3y2zUcP~kNIxR=HnK=Z z$Wk>Vx#^8P+vXHHZAm8UFFR3!#hHtX@Y<}(s$-Omy#$v~zLk0N7ajAJ`o~JX()PFc zWrpRbuu*pK0Y{Qv34&GzdRHoS@k8)D4bmvj40_&)M`F5^D#&F=t-fRWF}}{L+uiU-6_d--48;;BRMD~TQn3cBij`+7B^`ye zsH$AndXoEoe5G+SztfZ>ycU7WwiDI7j(Hy<<)HI8pVpN-D@n?jWThZq|4u{WT}l92 zgM;60dekYz?-Rl2H}NbCJEz1jbe>FP6mCEO|JH z3_(<5pMGGP-K>)xQsP2Z@yxwywe=+~J8hr?y<61l@QJh!w3q+x(#_Sz9{Bx!pLVXL z{iT(lg=r-K!a?=*bUB9|;0w>|#mOz~OgdS&|qCbH}A(#|zMe z6uhN4%e@WH%s+CNx4`g<@yk+@jM2&i3I*YUczoxe{`UFds_i7|K$3OrDWvUK^)PS? z(^0gc@Mr-vEMRId6m`k1!K4hmkN3)Qk5^@QXnC&?+bWtOgAP#?ryk z-yqkXeE_ZvHcB`Ny#azmP1R>8^$}PRZmr+)@s90MQEgqYX4H|wG8~Ib$fDbyeKRg zCr8v{0HDv)uS^-HK1K0?s1#GqxSF3QK#JA|7|!-3K+AsTY$58G27<7Yzi!9C&IH3NshKKtMbEHyh%yHtJl3+Aey;Lh59(yqb??B4IeD zm9F)fMrB^tbIcgRMuM#3d^gvtS4S7aPR#7$h;)>PH|;*1>MMn6A&JiwkKa5Ur9(F% zL1dS_1Db1u`Yo_*JP-F_C^XB9Z1L%C4q+orHgXL8I1Qzx`W4jrt?5EU|8G;!NSzWeNG&Hjli{v-u-D zK|+c?Ehk)<>H{WSI-Kn-rf=uD{+^_AaB*JD!npc%U;;R6;)=QgB=CEuocaaljF4O^ zzh3^FZZYf2_(J=uj?=7+#$yjMqav7#SK`)IPa+SN+=qlo_e!s_>W_|fWSCEG>IbO+ z4~)$s6yV~rwtl@A73o)$Yk~A`&@)zpUu5o!>pQ^bK5JG@s%yBlD8XJoz4WyhRr{-` z?Y1%AV;Q(Y+WnWiWpoZI&hV+9#4!9`FijOI@(C?1UzJ^>n9lL#QAP-l!i{zRSv<6R z-q_H#O;B*_X_3TXT$HKUC@(K30Wj4E%Fq<+eqfFlpWALXdOM@zUE?2&^x{Qy^^Dtt z*Y?F&^c#zfut^`~ypB85(1^?KWviDYa?{pmRuWi<*D~0!==#k1&d;P@9dzR${4gPB zwpXZ4yV+KSPcXZie_65QSFS_9K!xMM7Tp>3_QvsJ%!ks=-y`(=P~s!T>LVL`=9Fn( zwrA;<@ShpH%kZK^?dCHz9;K;XWzc*$k8w!=)r;%MyJB`A{(L~!RKHz5kLw!7l}#vm zfdT(gIdpqd2PW;L{|mA*)jiC@ld6k!y~x7Vq+SD5%{FE28WGgeY&{kY))D6f*D25Q zZIKpb)^m&1>KPLxb=G4OC^kX6rCPowoo~yKCR>iMApU@GvgktHya9$ou^;6|xY1)2 z77Yy*2*QhNRl*Z61(u(lX+Cs`!LhAByn$as6T5%IiG(Yp|Eglf-rG+vBMiH zNSRL~4z>Ds_`*DKHWA$IFyjUaiNWXB=oRPVpNREz~ zJdb0>;6p5v6{Ap$$6i?8IF(M#@^o+V%BY6TpW3(m|8$-~te>WSGA)dn=IQI+0JCc+ z1Y5UG&yN3{fgyr)pIgpUQ2yMG@mf>~r-@em=hB4Fs zPb*keoJx*#qEzubR$|G;*rVNlJ}u6i+w3bM2#6>C|3n4uC`O>oe;pP>cTvtnX++y$ zFws|ab+tA7kWz5b7Keh1RemB!_9(Q5T@M&c7%-2FA?<6G&u6~%6Ya&Z<`zguZ-j1N zUEO57^4w-*X9xj--;nh%YI{#dM+)aj25BoK?+CuStuN0U+pt}!hZAcsK7(+$L-+A| zi75A`YLcPLxgP>|q589cvPj-(Q-~QFwVzNdrq#xNZy(E{6RzPeFY#v$sNQj|a;fsnxzI(QS z{VxM!EhB2fwQ1s@ODoItDdL!WmT2NhHhUwuspBfFUp5T@DIKRY>vG>{lLz)G7BuoJ zwpEerKA-82becp1o*+DJ>_L7^2=fnU_9O77RM<8@$jNktpD?X$roUS71EkVyD%j1m zi;9B(0p=z`tb2#kAf~F~b4j)G>2^Cov%uDKasoo}w8VVriKr*Tw%&Zqj7~!Sy7;1^ zYXoZCSciBN^qHn`ZBGtWsl93LukGbpBV!*@Rb@_{ngsW#*s99n=UBvfoEUa;`FK47AVK3Z(Kk(`VMK%yB0isQfAzy_3+`v+SvC`vx<*mRenZ{rYe)+FRhOGb8<>o1JfoC4lLp|Q8h!ZVWpYp z07yBY#DyLjqm#Ft%nC9?=7gD;Q5ew0z{kR7g;rohjNHvfHj3lzM9_A+B0g#t*@*@9 z{}HX0C=Zbt-1H1+v=)mJxzxka&}Zhp+WrDpM_JLG{nPm;I$-s3wqsAM49srLc&@FG zsSi5S^wPxDXRWkHj_AgJiOi0$SLF4XOF4+)uII;p@9csmNs#=Xu4Mh=zwZ!?83ZP2 zzXTmw?U#$InVqt;gQJO)TX9nQFNFeHunGU#0U(YKcfCc z84#4Am^@i|WI`3q8)xJJ+WL)Ocu)OW2EQ`trvMLoSx7zacwbm6zN#CgSZU@pQ&aCR zzPAo}yMO;2Yk{QA8Ljy|n6|eiR65#dv@I{WPE?jW&`jF2*oHy1oZ>3f(Lw{$22i%J z$ZZ{W>v0DF&zlND9Quc`Ob->B+m;Wh#&kr5&d1KptP&lKZ9ffd_z-{i1>s?(MC!Kc zlN4XC!04kblxYWJQI%0fNorJ=_(cb@oSD@zFgPu`gNv;sJ&Wo;RFc77Cbj}ZF(=}_ zh1nhC;t&HEzIbjDwXMUM;e~)lHeGv;tp?ha{OFqb#^J_IjDbO#@TZH90(P5p*I5hvP54 zxh0t^54jbYv)5d@)6zndct=vo?){V~T9*+g0?@lE_Ss9^nBNUh9nOK$dv>AWhxfFD z6#^xKpSd@D+*JeQIFJmZj}rJa8ls@5H2WI&ZSG5fxHg^_xoapOW%| zOow14uOw#3p6V1%SNXsjPT39#z4-#;Op=pZXA{=Qs?W9GHMIeh)t^7o0(woLngo8H z4+<`;3k_TF3ii8&u70}@15*aHJ6uf>^L}bt?G_vGHDOJ#Bov{K;>*h3QRG}&gQA@e z9uuwy{Gu;!pid-0$Sm*--v8_BhG$5_$izneQaowLRi9<@l0X3jTqMppT7(t&mgqZd zDr(dm2mtDIXaq9!9H6->&ZG}aZPHH0aT{I$=!SpgV87(Dkm)+bc$OZ3T-qn z!OMiD!w1mEJvir zW2aB4yS38ZKex_!?|*;5l|zc^%zwxkMacgz)ng?gr$HrASK=q_C1C*z{EtQAsZzj) zn*sykJ8fjxA4I<3d*+5lhOqoVgp!?FJjzN0Y?J=AZu#rr?qUAAdP^kq z!-%j2#;2oW!dx)?7og3^T15{9j>1Wj-ZG`KT3Kyn$y9=lHG4H9e)>KgFRGv=@ zc=wADdn#VCmndt<5**Fy^goF*{V1TuD`h;j(UT&s-&L=ek|zL~ziK8}$2jZC2=^h57nb&+Xj0;6SK0M{Not zdZz(j4-L_ilW$;OzN@|ih7mQU2i-~jJ|$tSoAseoPDM>*%W1v2)MgWKlT^6ZZHGNF z8c*EwJ6_0X#_|qDK*Y&GQL+Wb5n00*6lHD1u^afa915W- zT?Loj+aB5k@$jc%8FKd!@1QnC~E88_D_bL04aMukP?cxyVom601|3fVoQoI-RZwN7@6Q2ln#~spKR=Ry(6IxzC zF#%G+G2D|id5_3Z6hUrCG9IDR-DvGwThMI#;US{nZ6p)-TOnW1-kx0TTX2w&(1xm(aP0F71hR_K*TMY<5a+Phx^w{W=@t17gH^mSK(im&ZG=( zHY+&j8`#KC*)CXO1mRNQ2prSNvye;Fm5%5KQCx; z+dA2~9tVLR*2#}wl3kX<%G~y*mW&hYC(@b49;C3o^Z~v_7$_x*N|I|v`&i45IX|B1=4vaVd3PpNY;;~A ztC*Q@XS!v7{8;phXUsnbA-TMXmOWsCxte$qib6tBnljH_wrg(qy)J~r(YKJKiI^@L z32i1FU~UBL+>rPfVS4sWYUk4F-yrQH&d^$snQ+bh=Grrl*yp_Y6P_G42ksY7{XDy!@BpD zR7o?eFWUQz?llUyQc1AcFyYNn=wV8H2Y518w=C)>qG}Dt!QVs|`{G*hTt>yKL6|Aws-73L-7Tq6n*O^57tyDvcRy5%UYtiLUv~R9V`;&h>u37{T3v< zEBXKCudNlzz882L^h?Hd@5OHmzJA%W>qTRDqg3I?%i+B{zU6xQGfmPHm>A*ke=Wu%L&yh?jK4PyH&G0^GizJmh0C&7taf*Z*5)C+PrUhW`)J}iYwoBdLQi! zymZKrJCpl-q=9Zvghi#~YAfIYXmtHkldpVts$g2*daUr-xl%9PhOn4}vooBx z>sA*WndWYo;?1g_Qz?|5Q#tKlD@&m0iOKa%0)at}MK@K>9kr5nK3KR%deeuEts7sf z9Dg_AUd*L9mK#SdF{`(~aW#FXyi>J;`E;$gPED!!y#?=?Rxim}-+3Z4@##G+!MZhz z50xuMN%s8Om$^jdSm8%LMah3l>iHvAE_{D<+mdXX^!xL>&-kvnt+rg?s><9=mrW;J z&Qr=2>`l|(aq0Wtdz>+x-?%TZ)a{LWl(}xNs*L|lqZ_YV_D(#0Z&u%0rJSw3cc&kg zTTm!^QnsnpO-XUv+E03`riaII-*pXraqE>~$i|mBB|)aSMoyPc3anhatYF66U$rZK z@Pj%~f{}?Yf+zRPUCBB*p(;Xgvemp~mc!G9W=>u>PmIY$U~=F*naQ;RqLUx26kvti zt^R+WC=uynoD+HdCGWoQ!JlHzW4QPvi zy~J8z4dn~9WW=t+?#W_cFh)`QKm$p!HY@l>rpW?}M47_1;Syepv}BO) z$+1T4#Ch@z3~DGQ#h6Y$uviIrMFm75 z_%L*!57z*(4vNChmOzE>vXH}}85rgOPp3!q)hcU-$qx2Xliyn_gY1-rpH~bFEJqZh zgzZ5py}_#B$KL`~*`cTsa%7ln@8|(`KjI`-1_pf;RUXchA1oD}+`rUR8gbAhx`j5A z?=OvI1)s+^*>RaD(_NscOXVhOdMbiVM;w*|Je&{3bX^~yLfOd=mdVS&4_g5`R2N0j zt5C2L43-axH1|&#=Wr3=B#r3YSm5zuZm+d94eoZBHsE zKUgk1*`f-PT@V9^3=9e=25qVaDwLVLbA`MNVnm36K^{dBLpRu2{@vi5DT5dWK~EIW&pHfkaU4roNf6g>=uCr>T__Rcg`=}3c15@4P_ a%EQ2*fnt2> Date: Wed, 26 Feb 2025 15:11:31 -0500 Subject: [PATCH 11/39] =?UTF-8?q?=F0=9F=A4=96=20Update=20Android=20Gradle?= =?UTF-8?q?=20Plugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version updated to the latest --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69bdce02c..f48f459db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # Project version_code = "30200" version_name = "3.2.0" -android_gradle_plugin = "8.8.1" +android_gradle_plugin = "8.8.2" kotlin = "2.1.10" android_sdk_compile = "35" android_sdk_target = "34" From d5be39c1718b9f6fa9425e83c717e18404030e3e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:28:17 +0000 Subject: [PATCH 12/39] fix(deps): update dependency androidx.activity:activity to v1.10.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f48f459db..b3b5bb540 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ kotlinx_collections_immutable = "0.3.8" kotlinx_datetime = "0.6.2" # AndroidX -activity = "1.10.0" +activity = "1.10.1" core = "1.15.0" appcompat = "1.7.0" corektx = "1.15.0" From 92157e6a30f670dc20a774c17c22403b01b34a22 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:47:17 +0000 Subject: [PATCH 13/39] fix(deps): update dependency androidx.datastore:datastore-preferences-core to v1.1.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3b5bb540..9f6faf7e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ core = "1.15.0" appcompat = "1.7.0" corektx = "1.15.0" play_delivery = "2.1.0" -datastore = "1.1.2" +datastore = "1.1.3" glance = "1.1.1" workmanager = "2.10.0" lifecycle_viewmodel = "2.8.7" From 00150fb9b599d4516a4ba148d6ed92b67eefc42c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 21:04:27 +0000 Subject: [PATCH 14/39] fix(deps): update dependency androidx.activity:activity-compose to v1.10.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f6faf7e5..8a9e077f6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,7 +37,7 @@ annotation = "1.9.1" # Compose compose_bom = "2025.02.00" -compose_activity = "1.10.0" +compose_activity = "1.10.1" compose_compiler = "1.8.0-alpha03" compose_windowsizeclass = "0.5.0" compose_navigation = "2.8.0-alpha13" From 8347dac167808a5dd5c0081e885b6e79645569da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 07:04:51 +0000 Subject: [PATCH 15/39] fix(deps): update moko.permissions to v0.19.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8a9e077f6..72f00d75c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ sqldelight = "2.0.2" # Moko moko = "0.16.1" -moko_permissions = "0.19.0" +moko_permissions = "0.19.1" # Test test_junit = "4.13.2" From 86e50b7a1dc6308d61d5e14e4f583059cce2611e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Mar 2025 01:32:25 +0000 Subject: [PATCH 16/39] fix(deps): update dependency com.android.tools.build:gradle to v8.9.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 72f00d75c..4b29bd046 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # Project version_code = "30200" version_name = "3.2.0" -android_gradle_plugin = "8.8.2" +android_gradle_plugin = "8.9.0" kotlin = "2.1.10" android_sdk_compile = "35" android_sdk_target = "34" From 93e41db26b7cd7c3fb1fb68906f9ba85e7bc4e9f Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Fri, 7 Mar 2025 17:46:09 -0500 Subject: [PATCH 17/39] =?UTF-8?q?=E2=9A=B1=EF=B8=8F=20Refactor=20URI=20con?= =?UTF-8?q?version=20in=20AndroidDeepLink?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced `Uri.parse` with `toUri` for cleaner and more idiomatic URI handling using AndroidX. This is a new AGP lint warning. --- .../com/escodro/navigationapi/deeplink/AndroidDeepLink.kt | 4 ++-- .../com/escodro/preference/provider/AndroidTrackerProvider.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/features/navigation-api/src/androidMain/kotlin/com/escodro/navigationapi/deeplink/AndroidDeepLink.kt b/features/navigation-api/src/androidMain/kotlin/com/escodro/navigationapi/deeplink/AndroidDeepLink.kt index 962da3020..0f7a871b0 100644 --- a/features/navigation-api/src/androidMain/kotlin/com/escodro/navigationapi/deeplink/AndroidDeepLink.kt +++ b/features/navigation-api/src/androidMain/kotlin/com/escodro/navigationapi/deeplink/AndroidDeepLink.kt @@ -1,7 +1,7 @@ package com.escodro.navigationapi.deeplink import android.content.Intent -import android.net.Uri +import androidx.core.net.toUri import com.escodro.navigationapi.destination.Destination /** @@ -13,7 +13,7 @@ object AndroidDeepLink { /** * Returns the [Intent] to the home screen. */ - fun homeIntent(): Intent = Intent(Intent.ACTION_VIEW, Uri.parse(Destination.URI)) + fun homeIntent(): Intent = Intent(Intent.ACTION_VIEW, Destination.URI.toUri()) /** * Returns the [Intent] to the task detail screen. diff --git a/features/preference/src/androidMain/kotlin/com/escodro/preference/provider/AndroidTrackerProvider.kt b/features/preference/src/androidMain/kotlin/com/escodro/preference/provider/AndroidTrackerProvider.kt index 09c19a01e..504a04515 100644 --- a/features/preference/src/androidMain/kotlin/com/escodro/preference/provider/AndroidTrackerProvider.kt +++ b/features/preference/src/androidMain/kotlin/com/escodro/preference/provider/AndroidTrackerProvider.kt @@ -2,8 +2,8 @@ package com.escodro.preference.provider import android.content.Context import android.content.Intent -import android.net.Uri import androidx.compose.runtime.Composable +import androidx.core.net.toUri import com.escodro.splitinstall.LoadFeature internal class AndroidTrackerProvider(private val context: Context) : TrackerProvider { @@ -19,7 +19,7 @@ internal class AndroidTrackerProvider(private val context: Context) : TrackerPro // https://issuetracker.google.com/issues/183677219 val intent = Intent(Intent.ACTION_VIEW).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK - data = Uri.parse(TrackerDeepLink) + data = TrackerDeepLink.toUri() `package` = context.packageName } context.startActivity(intent) From d96cd24067c922805faf3fa70f407a2ed515126f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 20:12:54 +0000 Subject: [PATCH 18/39] chore(deps): update dependency com.autonomousapps.dependency-analysis to v2.11.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4b29bd046..403605557 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ android_sdk_target = "34" android_sdk_min = "24" # Plugins -dependencyanalysis = "2.10.1" +dependencyanalysis = "2.11.0" # General dependencies logging = "3.0.5" From 7532cc22d4cc6fe31bf47cb9ed06702a18f199ec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:04:26 +0000 Subject: [PATCH 19/39] chore(deps): update dependency com.autonomousapps.dependency-analysis to v2.12.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 403605557..49fd99673 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ android_sdk_target = "34" android_sdk_min = "24" # Plugins -dependencyanalysis = "2.11.0" +dependencyanalysis = "2.12.0" # General dependencies logging = "3.0.5" From 9e58a6afd35e61bda65afd454ccecd784e5cd0a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:37:18 +0000 Subject: [PATCH 20/39] fix(deps): update dependency androidx.compose:compose-bom to v2025.03.00 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 49fd99673..9c6230dfd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ lifecycle_viewmodel = "2.8.7" annotation = "1.9.1" # Compose -compose_bom = "2025.02.00" +compose_bom = "2025.03.00" compose_activity = "1.10.1" compose_compiler = "1.8.0-alpha03" compose_windowsizeclass = "0.5.0" From 2a4e58bdfee10dc8b9749b055eb23432037684dd Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sat, 22 Feb 2025 09:06:59 -0500 Subject: [PATCH 21/39] =?UTF-8?q?=F0=9F=AA=9F=20Remove=20WindowSizeClass?= =?UTF-8?q?=20third-party=20library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing the library that had a custom implementation for WindowSizeClass. Next commits will add the new Compose Multiplatform adaptive libraries and improve the adaptive layout. --- .../escodro/home/presentation/HomeScreen.kt | 52 +++---------------- gradle/libs.versions.toml | 2 - libraries/appstate/build.gradle.kts | 2 - .../com/escodro/appstate/AlkaaAppState.kt | 37 ++----------- .../escodro/shared/AlkaaMultiplatformApp.kt | 5 +- .../com/escodro/alkaa/fake/AppStateFake.kt | 2 - 6 files changed, 10 insertions(+), 90 deletions(-) diff --git a/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt b/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt index f5a389f26..4cdb7c079 100644 --- a/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt +++ b/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt @@ -22,9 +22,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationRail -import androidx.compose.material3.NavigationRailItem -import androidx.compose.material3.NavigationRailItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -117,14 +114,7 @@ private fun AlkaaHomeScaffold( WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal), ), ) { - if (appState.shouldShowNavRail) { - AlkaaNavRail( - currentSection = currentSection, - onSectionSelect = setCurrentSection, - items = navItems, - modifier = Modifier.consumeWindowInsets(paddingValues), - ) - } + // TODO implement with NavigationSuiteScaffold Column(Modifier.fillMaxSize()) { Navigation( startDestination = HomeDestination.TaskList, @@ -134,45 +124,15 @@ private fun AlkaaHomeScaffold( } }, bottomBar = { - if (appState.shouldShowBottomBar) { - AlkaaBottomNav( - currentSection = currentSection, - onSectionSelect = setCurrentSection, - items = navItems, - ) - } + AlkaaBottomNav( + currentSection = currentSection, + onSectionSelect = setCurrentSection, + items = navItems, + ) }, ) } -@Composable -private fun AlkaaNavRail( - currentSection: TopLevel, - onSectionSelect: (TopLevel) -> Unit, - items: ImmutableList, - modifier: Modifier = Modifier, -) { - NavigationRail(modifier = modifier) { - items.forEach { section -> - val selected = section == currentSection - NavigationRailItem( - selected = selected, - onClick = { onSectionSelect(section) }, - alwaysShowLabel = true, - icon = { Icon(imageVector = section.icon, contentDescription = null) }, - label = { Text(stringResource(section.title)) }, - colors = NavigationRailItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer, - unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, - indicatorColor = MaterialTheme.colorScheme.primaryContainer, - ), - ) - } - } -} - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AlkaaTopBar(currentSection: TopLevel) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c6230dfd..168f48b9c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,7 +39,6 @@ annotation = "1.9.1" compose_bom = "2025.03.00" compose_activity = "1.10.1" compose_compiler = "1.8.0-alpha03" -compose_windowsizeclass = "0.5.0" compose_navigation = "2.8.0-alpha13" # Koin @@ -104,7 +103,6 @@ compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = compose_ui = { module = "androidx.compose.ui:ui" } compose_icons = { module = "androidx.compose.material:material-icons-extended" } compose_material3 = { module = "androidx.compose.material3:material3" } -compose_windowsizeclass = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "compose_windowsizeclass" } compose_navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "compose_navigation" } # Dynamic Feature don't recognize BOM diff --git a/libraries/appstate/build.gradle.kts b/libraries/appstate/build.gradle.kts index d32aacd7a..5f1359e9d 100644 --- a/libraries/appstate/build.gradle.kts +++ b/libraries/appstate/build.gradle.kts @@ -17,8 +17,6 @@ kotlin { implementation(compose.runtime) implementation(libs.compose.navigation) - - api(libs.compose.windowsizeclass) } } } diff --git a/libraries/appstate/src/commonMain/kotlin/com/escodro/appstate/AlkaaAppState.kt b/libraries/appstate/src/commonMain/kotlin/com/escodro/appstate/AlkaaAppState.kt index 123a2e6da..e4e9eb73e 100644 --- a/libraries/appstate/src/commonMain/kotlin/com/escodro/appstate/AlkaaAppState.kt +++ b/libraries/appstate/src/commonMain/kotlin/com/escodro/appstate/AlkaaAppState.kt @@ -1,8 +1,5 @@ package com.escodro.appstate -import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember @@ -23,22 +20,9 @@ import kotlinx.coroutines.flow.mapLatest */ @Stable data class AlkaaAppState( - private val isCompactMode: Boolean, override val navHostController: NavHostController, ) : AppState { - /** - * Verifies if the bottom bar should be shown. - */ - override val shouldShowBottomBar: Boolean - get() = isCompactMode - - /** - * Verifies if the navigation rail should be shown. - */ - override val shouldShowNavRail: Boolean - get() = !shouldShowBottomBar - override val currentTopDestination: Flow = navHostController.currentTopLevelFlow() @@ -53,16 +37,6 @@ data class AlkaaAppState( interface AppState { - /** - * Verifies if the bottom bar should be shown. - */ - val shouldShowBottomBar: Boolean - - /** - * Verifies if the navigation rail should be shown. - */ - val shouldShowNavRail: Boolean - /** * The [NavHostController] used to navigate between destinations. */ @@ -82,16 +56,11 @@ interface AppState { /** * Function to remember a [AlkaaAppState]. * - * @param windowSizeClass the window size class from current device + * @param navHostController the [NavHostController] used to navigate between destinations */ @Composable fun rememberAlkaaAppState( - windowSizeClass: WindowSizeClass, navHostController: NavHostController = rememberNavController(), -): AlkaaAppState { - val isCompactMode = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact || - windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact - return remember(isCompactMode, navHostController) { - AlkaaAppState(isCompactMode, navHostController) - } +): AlkaaAppState = remember(navHostController) { + AlkaaAppState(navHostController) } diff --git a/shared/src/commonMain/kotlin/com/escodro/shared/AlkaaMultiplatformApp.kt b/shared/src/commonMain/kotlin/com/escodro/shared/AlkaaMultiplatformApp.kt index 2387c17c9..8168aa15e 100644 --- a/shared/src/commonMain/kotlin/com/escodro/shared/AlkaaMultiplatformApp.kt +++ b/shared/src/commonMain/kotlin/com/escodro/shared/AlkaaMultiplatformApp.kt @@ -1,8 +1,6 @@ package com.escodro.shared import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -15,11 +13,10 @@ import com.escodro.home.presentation.Home import com.escodro.shared.model.AppThemeOptions import org.koin.compose.koinInject -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @Composable fun AlkaaMultiplatformApp( modifier: Modifier = Modifier, - appState: AppState = rememberAlkaaAppState(windowSizeClass = calculateWindowSizeClass()), + appState: AppState = rememberAlkaaAppState(), onThemeUpdate: (isDarkTheme: Boolean) -> Unit = {}, ) { val isDarkTheme = rememberIsDarkTheme() diff --git a/shared/src/commonTest/kotlin/com/escodro/alkaa/fake/AppStateFake.kt b/shared/src/commonTest/kotlin/com/escodro/alkaa/fake/AppStateFake.kt index d6bbb8a86..88eec74c0 100644 --- a/shared/src/commonTest/kotlin/com/escodro/alkaa/fake/AppStateFake.kt +++ b/shared/src/commonTest/kotlin/com/escodro/alkaa/fake/AppStateFake.kt @@ -12,8 +12,6 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.mapLatest internal class AppStateFake(override val navHostController: NavHostController) : AppState { - override val shouldShowBottomBar: Boolean = true - override val shouldShowNavRail: Boolean = false @OptIn(ExperimentalCoroutinesApi::class) override val currentTopDestination: Flow = From 0589fca2e0c32479c6536e7ecb7fed54fe21f43d Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sat, 22 Feb 2025 09:08:15 -0500 Subject: [PATCH 22/39] =?UTF-8?q?=F0=9F=8F=B8=20Update=20the=20Open=20Sour?= =?UTF-8?q?ce=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Once we updated some libraries recently, and we are removing the WindowSizeClass, the file was updated to reflect those changes. --- .../files/aboutlibraries.json | 531 +++++++++++++----- 1 file changed, 405 insertions(+), 126 deletions(-) diff --git a/resources/src/commonMain/composeResources/files/aboutlibraries.json b/resources/src/commonMain/composeResources/files/aboutlibraries.json index 41fe23f8c..8bd0037d6 100644 --- a/resources/src/commonMain/composeResources/files/aboutlibraries.json +++ b/resources/src/commonMain/composeResources/files/aboutlibraries.json @@ -229,14 +229,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.4.4", + "artifactVersion": "1.5.0-beta03", "description": "Standalone efficient collections.", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "collections", - "website": "https://developer.android.com/jetpack/androidx/releases/collection#1.4.4", + "website": "https://developer.android.com/jetpack/androidx/releases/collection#1.5.0-beta03", "licenses": [ "Apache-2.0" ], @@ -252,14 +252,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.4.4", + "artifactVersion": "1.5.0-beta03", "description": "Kotlin extensions for 'collection' artifact", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Collections Kotlin Extensions", - "website": "https://developer.android.com/jetpack/androidx/releases/collection#1.4.4", + "website": "https://developer.android.com/jetpack/androidx/releases/collection#1.5.0-beta03", "licenses": [ "Apache-2.0" ], @@ -275,14 +275,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose animation library", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Animation", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-animation#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-animation#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -298,14 +298,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Animation engine and animation primitives that are the building blocks of the Compose animation library", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Animation Core", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-animation#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-animation#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -321,14 +321,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Higher level abstractions of the Compose UI primitives. This library is design system agnostic, providing the high-level building blocks for both application and design-system developers", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Foundation", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -344,14 +344,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose layout implementations", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Layouts", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -367,14 +367,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.3.1", + "artifactVersion": "1.4.0-alpha07", "description": "Compose Material You Design Components library", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Material3 Components", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-material3#1.3.1", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-material3#1.4.0-alpha07", "licenses": [ "Apache-2.0" ], @@ -390,14 +390,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose Material Design Components library", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Material Components", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -459,14 +459,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Material ripple used to build interactive components", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Material Ripple", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -482,14 +482,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Tree composition support for code generated by the Compose compiler plugin and corresponding public API", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Runtime", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -505,14 +505,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose components that allow saving and restoring the local ui state", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Saveable", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -528,14 +528,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose UI primitives. This library contains the primitives that form the Compose UI Toolkit, such as drawing, measurement and layout.", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose UI", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -551,14 +551,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose classes related to dimensions without units", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Geometry", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -574,14 +574,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose graphics", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Graphics", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -597,14 +597,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose testing library", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Testing", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -643,14 +643,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose testing library that should be added as a debugImplementation dependency to add properties to the debug manifest necessary for testing an application", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Testing manifest dependency", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -666,14 +666,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose Text primitives and utilities", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose UI Text", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -689,14 +689,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose tooling library API. This library provides the API required to declare @Preview composables in user apps.", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose UI Preview Tooling", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -712,14 +712,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Compose classes for simple units", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Unit", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -735,14 +735,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.7.8", + "artifactVersion": "1.8.0-beta01", "description": "Internal Compose utilities used by other modules", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, "name": "Compose Util", - "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.7.8", + "website": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.8.0-beta01", "licenses": [ "Apache-2.0" ], @@ -1120,14 +1120,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.3.0", + "artifactVersion": "1.4.0", "description": "Core library to enable emoji compatibility in Kitkat and newer devices to avoid the empty emoji characters.", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, - "name": "Android Emoji2 Compat", - "website": "https://developer.android.com/jetpack/androidx/releases/emoji2#1.3.0", + "name": "Emoji2", + "website": "https://developer.android.com/jetpack/androidx/releases/emoji2#1.4.0", "licenses": [ "Apache-2.0" ] @@ -1140,14 +1140,14 @@ "name": "The Android Open Source Project" } ], - "artifactVersion": "1.3.0", - "description": "View helpers for Emoji2", + "artifactVersion": "1.4.0", + "description": "Provide helper classes for Emoji2 views.", "scm": { "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", "url": "https://cs.android.com/androidx/platform/frameworks/support" }, - "name": "Android Emoji2 Compat view helpers", - "website": "https://developer.android.com/jetpack/androidx/releases/emoji2#1.3.0", + "name": "Emoji2 Views Helper", + "website": "https://developer.android.com/jetpack/androidx/releases/emoji2#1.4.0", "licenses": [ "Apache-2.0" ] @@ -1336,6 +1336,29 @@ "name": "The Android Open Source Project" } }, + { + "uniqueId": "androidx.graphics:graphics-shapes-android", + "funding": [], + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "artifactVersion": "1.0.1", + "description": "create and render rounded polygonal shapes", + "scm": { + "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", + "url": "https://cs.android.com/androidx/platform/frameworks/support" + }, + "name": "Graphics Shapes", + "website": "https://developer.android.com/jetpack/androidx/releases/graphics#1.0.1", + "licenses": [ + "Apache-2.0" + ], + "organization": { + "name": "The Android Open Source Project" + } + }, { "uniqueId": "androidx.interpolator:interpolator", "funding": [], @@ -1836,6 +1859,29 @@ "name": "The Android Open Source Project" } }, + { + "uniqueId": "androidx.performance:performance-annotation-android", + "funding": [], + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "artifactVersion": "1.0.0-alpha01", + "description": "Provides source annotations for performance optimizations.", + "scm": { + "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", + "url": "https://cs.android.com/androidx/platform/frameworks/support" + }, + "name": "Performance - Annotation", + "website": "https://developer.android.com/jetpack/androidx/releases/performance#1.0.0-alpha01", + "licenses": [ + "Apache-2.0" + ], + "organization": { + "name": "The Android Open Source Project" + } + }, { "uniqueId": "androidx.profileinstaller:profileinstaller", "funding": [], @@ -2174,6 +2220,22 @@ "Apache-2.0" ] }, + { + "uniqueId": "androidx.test:rules", + "funding": [], + "developers": [ + { + "name": "The Android Open Source Project" + } + ], + "artifactVersion": "1.4.0", + "description": "The AndroidX Test Library provides an extensive framework for testing Android apps", + "name": "AndroidX Test Library", + "website": "https://developer.android.com/testing", + "licenses": [ + "Apache-2.0" + ] + }, { "uniqueId": "androidx.test:runner", "funding": [], @@ -2310,49 +2372,6 @@ "Apache-2.0" ] }, - { - "uniqueId": "androidx.window.extensions.core:core", - "funding": [], - "developers": [ - { - "name": "The Android Open Source Project" - } - ], - "artifactVersion": "1.0.0", - "description": "The Core APIs for Window Manager Library Extensions", - "scm": { - "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", - "url": "https://cs.android.com/androidx/platform/frameworks/support" - }, - "name": "Jetpack WindowManager library Core Extensions", - "website": "https://developer.android.com/jetpack/androidx/releases/window-extensions-core#1.0.0", - "licenses": [ - "Apache-2.0" - ] - }, - { - "uniqueId": "androidx.window:window", - "funding": [], - "developers": [ - { - "name": "The Android Open Source Project" - } - ], - "artifactVersion": "1.2.0", - "description": "WindowManager Jetpack library. Currently only provides additional functionality on foldable devices.", - "scm": { - "connection": "scm:git:https://android.googlesource.com/platform/frameworks/support", - "url": "https://cs.android.com/androidx/platform/frameworks/support" - }, - "name": "WindowManager", - "website": "https://developer.android.com/jetpack/androidx/releases/window#1.2.0", - "licenses": [ - "Apache-2.0" - ], - "organization": { - "name": "The Android Open Source Project" - } - }, { "uniqueId": "androidx.work:work-runtime", "funding": [], @@ -2588,6 +2607,28 @@ "Apache-2.0" ] }, + { + "uniqueId": "com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework", + "funding": [], + "developers": [ + { + "organisationUrl": "https://www.google.com", + "name": "Casey Burkhardt" + } + ], + "artifactVersion": "4.1.1", + "description": "Library used to test for common accessibility issues.", + "scm": { + "connection": "scm:git:git@github.com:google/Accessibility-Test-Framework-for-Android.git", + "url": "https://github.com/google/Accessibility-Test-Framework-for-Android", + "developerConnection": "scm:git:git@github.com:google/Accessibility-Test-Framework-for-Android.git" + }, + "name": "Accessibility Test Framework", + "website": "https://github.com/google/Accessibility-Test-Framework-for-Android", + "licenses": [ + "Apache-2.0" + ] + }, { "uniqueId": "com.google.android.gms:play-services-basement", "funding": [], @@ -2636,12 +2677,12 @@ "uniqueId": "com.google.code.findbugs:jsr305", "funding": [], "developers": [], - "artifactVersion": "2.0.2", + "artifactVersion": "3.0.2", "description": "JSR305 Annotations for Findbugs", "scm": { - "connection": "scm:svn:http://findbugs.googlecode.com/svn/trunk/", - "url": "http://findbugs.googlecode.com/svn/trunk/", - "developerConnection": "scm:svn:https://findbugs.googlecode.com/svn/trunk/" + "connection": "scm:git:https://code.google.com/p/jsr-305/", + "url": "https://code.google.com/p/jsr-305/", + "developerConnection": "scm:git:https://code.google.com/p/jsr-305/" }, "name": "FindBugs-jsr305", "website": "http://findbugs.sourceforge.net/", @@ -2649,12 +2690,67 @@ "Apache-2.0" ] }, + { + "uniqueId": "com.google.errorprone:error_prone_annotations", + "funding": [], + "developers": [], + "artifactVersion": "2.14.0", + "description": "Error Prone is a static analysis tool for Java that catches common programming mistakes at compile-time.", + "scm": { + "connection": "scm:git:https://github.com/google/error-prone.git", + "url": "https://github.com/google/error-prone", + "developerConnection": "scm:git:git@github.com:google/error-prone.git" + }, + "name": "error-prone annotations", + "website": "https://errorprone.info", + "licenses": [ + "Apache-2.0" + ], + "organization": { + "url": "http://www.google.com", + "name": "Google LLC" + } + }, + { + "uniqueId": "com.google.guava:failureaccess", + "funding": [], + "developers": [], + "artifactVersion": "1.0.1", + "description": "Contains\n com.google.common.util.concurrent.internal.InternalFutureFailureAccess and\n InternalFutures. Most users will never need to use this artifact. Its\n classes is conceptually a part of Guava, but they're in this separate\n artifact so that Android libraries can use them without pulling in all of\n Guava (just as they can use ListenableFuture by depending on the\n listenablefuture artifact).", + "scm": { + "connection": "scm:git:https://github.com/google/guava.git", + "url": "https://github.com/google/guava", + "developerConnection": "scm:git:git@github.com:google/guava.git" + }, + "name": "Guava InternalFutureFailureAccess and InternalFutures", + "website": "https://github.com/google/guava", + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "com.google.guava:guava", + "funding": [], + "developers": [], + "artifactVersion": "31.0.1-android", + "description": "Guava is a suite of core and expanded libraries that include\n utility classes, Google's collections, I/O classes, and\n much more.", + "scm": { + "connection": "scm:git:https://github.com/google/guava.git", + "url": "https://github.com/google/guava", + "developerConnection": "scm:git:git@github.com:google/guava.git" + }, + "name": "Guava: Google Core Libraries for Java", + "website": "https://github.com/google/guava", + "licenses": [ + "Apache-2.0" + ] + }, { "uniqueId": "com.google.guava:listenablefuture", "funding": [], "developers": [], - "artifactVersion": "1.0", - "description": "Contains Guava's com.google.common.util.concurrent.ListenableFuture class,\n without any of its other classes -- but is also available in a second\n \"version\" that omits the class to avoid conflicts with the copy in Guava\n itself. The idea is:\n\n - If users want only ListenableFuture, they depend on listenablefuture-1.0.\n\n - If users want all of Guava, they depend on guava, which, as of Guava\n 27.0, depends on\n listenablefuture-9999.0-empty-to-avoid-conflict-with-guava. The 9999.0-...\n version number is enough for some build systems (notably, Gradle) to select\n that empty artifact over the \"real\" listenablefuture-1.0 -- avoiding a\n conflict with the copy of ListenableFuture in guava itself. If users are\n using an older version of Guava or a build system other than Gradle, they\n may see class conflicts. If so, they can solve them by manually excluding\n the listenablefuture artifact or manually forcing their build systems to\n use 9999.0-....", + "artifactVersion": "9999.0-empty-to-avoid-conflict-with-guava", + "description": "An empty artifact that Guava depends on to signal that it is providing\n ListenableFuture -- but is also available in a second \"version\" that\n contains com.google.common.util.concurrent.ListenableFuture class, without\n any other Guava classes. The idea is:\n\n - If users want only ListenableFuture, they depend on listenablefuture-1.0.\n\n - If users want all of Guava, they depend on guava, which, as of Guava\n 27.0, depends on\n listenablefuture-9999.0-empty-to-avoid-conflict-with-guava. The 9999.0-...\n version number is enough for some build systems (notably, Gradle) to select\n that empty artifact over the \"real\" listenablefuture-1.0 -- avoiding a\n conflict with the copy of ListenableFuture in guava itself. If users are\n using an older version of Guava or a build system other than Gradle, they\n may see class conflicts. If so, they can solve them by manually excluding\n the listenablefuture artifact or manually forcing their build systems to\n use 9999.0-....", "scm": { "connection": "scm:git:https://github.com/google/guava.git", "url": "https://github.com/google/guava", @@ -2666,6 +2762,39 @@ "Apache-2.0" ] }, + { + "uniqueId": "com.google.j2objc:j2objc-annotations", + "funding": [], + "developers": [], + "artifactVersion": "1.3", + "description": "A set of annotations that provide additional information to the J2ObjC\n translator to modify the result of translation.", + "scm": { + "connection": "scm:svn:http://svn.sonatype.org/spice/tags/oss-parent-7", + "url": "http://svn.sonatype.org/spice/tags/oss-parent-7", + "developerConnection": "scm:svn:https://svn.sonatype.org/spice/tags/oss-parent-7" + }, + "name": "J2ObjC Annotations", + "website": "https://github.com/google/j2objc/", + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "com.google.protobuf:protobuf-javalite", + "funding": [], + "developers": [], + "artifactVersion": "3.19.1", + "description": "Lite version of Protocol Buffers library. This version is optimized for code size, but does\n not guarantee API/ABI stability.", + "scm": { + "connection": "scm:git:https://github.com/protocolbuffers/protobuf.git", + "url": "https://github.com/protocolbuffers/protobuf" + }, + "name": "Protocol Buffers [Lite]", + "website": "https://developers.google.com/protocol-buffers/", + "licenses": [ + "BSD-3-Clause" + ] + }, { "uniqueId": "com.mikepenz:aboutlibraries-compose-android", "funding": [], @@ -2674,7 +2803,7 @@ "name": "Mike Penz" } ], - "artifactVersion": "11.5.0", + "artifactVersion": "11.6.3", "description": "AboutLibraries automatically detects all dependencies of a project and collects their information including the license. Optionally visualising it via the provided ui components.", "scm": { "connection": "scm:git@github.com:mikepenz/AboutLibraries.git", @@ -2695,7 +2824,7 @@ "name": "Mike Penz" } ], - "artifactVersion": "11.5.0", + "artifactVersion": "11.6.3", "description": "AboutLibraries automatically detects all dependencies of a project and collects their information including the license. Optionally visualising it via the provided ui components.", "scm": { "connection": "scm:git@github.com:mikepenz/AboutLibraries.git", @@ -2771,27 +2900,6 @@ "name": "Square, Inc." } }, - { - "uniqueId": "dev.chrisbanes.material3:material3-window-size-class-multiplatform-android", - "funding": [], - "developers": [ - { - "name": "Chris Banes" - } - ], - "artifactVersion": "0.5.0", - "description": "Provides window size classes for building responsive UIs", - "scm": { - "connection": "scm:git:git://github.com/chrisbanes/material3-windowsizeclass-multiplatform.git", - "url": "https://github.com/chrisbanes/material3-windowsizeclass-multiplatform/", - "developerConnection": "scm:git:git://github.com/chrisbanes/material3-windowsizeclass-multiplatform.git" - }, - "name": "Compose Material 3 Window Size Class", - "website": "https://github.com/chrisbanes/material3-windowsizeclass-multiplatform/", - "licenses": [ - "Apache-2.0" - ] - }, { "uniqueId": "dev.icerock.moko:kswift-runtime-android", "funding": [], @@ -2905,7 +3013,7 @@ "name": "Aleksey Mikhailov" } ], - "artifactVersion": "0.18.1", + "artifactVersion": "0.19.0", "description": "Runtime permissions controls for mobile (android & ios) Kotlin Multiplatform development", "scm": { "connection": "scm:git:ssh://github.com/icerockdev/moko-permissions.git", @@ -2926,7 +3034,28 @@ "name": "Aleksey Mikhailov" } ], - "artifactVersion": "0.18.1", + "artifactVersion": "0.19.0", + "description": "Runtime permissions controls for mobile (android & ios) Kotlin Multiplatform development", + "scm": { + "connection": "scm:git:ssh://github.com/icerockdev/moko-permissions.git", + "url": "https://github.com/icerockdev/moko-permissions", + "developerConnection": "scm:git:ssh://github.com/icerockdev/moko-permissions.git" + }, + "name": "MOKO permissions", + "website": "https://github.com/icerockdev/moko-permissions", + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "dev.icerock.moko:permissions-notifications-android", + "funding": [], + "developers": [ + { + "name": "Aleksey Mikhailov" + } + ], + "artifactVersion": "0.19.0", "description": "Runtime permissions controls for mobile (android & ios) Kotlin Multiplatform development", "scm": { "connection": "scm:git:ssh://github.com/icerockdev/moko-permissions.git", @@ -3090,6 +3219,62 @@ "name": "JUnit" } }, + { + "uniqueId": "org.checkerframework:checker-compat-qual", + "funding": [], + "developers": [ + { + "organisationUrl": "https://www.cs.washington.edu/", + "name": "Michael Ernst" + }, + { + "organisationUrl": "http://uwaterloo.ca/", + "name": "Werner M. Dietl" + }, + { + "organisationUrl": "https://www.cs.washington.edu/research/plse/", + "name": "Suzanne Millstein" + } + ], + "artifactVersion": "2.5.5", + "description": "Checker Qual is the set of annotations (qualifiers) and supporting classes\n used by the Checker Framework to type check Java source code. Please\n see artifact:\n org.checkerframework:checker", + "scm": { + "connection": "https://github.com/typetools/checker-framework.git", + "url": "https://github.com/typetools/checker-framework.git" + }, + "name": "Checker Qual", + "website": "https://checkerframework.org", + "licenses": [ + "da5e524a4235731b9b96281090f5f82d", + "MIT" + ] + }, + { + "uniqueId": "org.checkerframework:checker-qual", + "funding": [], + "developers": [ + { + "organisationUrl": "https://www.cs.washington.edu/", + "name": "Michael Ernst" + }, + { + "organisationUrl": "https://www.cs.washington.edu/", + "name": "Suzanne Millstein" + } + ], + "artifactVersion": "3.22.1", + "description": "checker-qual contains annotations (type qualifiers) that a programmer\nwrites to specify Java code for type-checking by the Checker Framework.", + "scm": { + "connection": "scm:git:git://github.com/typetools/checker-framework.git", + "url": "https://github.com/typetools/checker-framework.git", + "developerConnection": "scm:git:ssh://git@github.com/typetools/checker-framework.git" + }, + "name": "Checker Qual", + "website": "https://checkerframework.org", + "licenses": [ + "MIT" + ] + }, { "uniqueId": "org.hamcrest:hamcrest-core", "funding": [], @@ -3147,7 +3332,7 @@ "name": "Compose Multiplatform Team" } ], - "artifactVersion": "1.1.0-alpha02", + "artifactVersion": "1.1.0-alpha03", "description": "Provides Bundle in Kotlin Multiplatform projects", "scm": { "connection": "scm:git:https://github.com/JetBrains/compose-jb.git", @@ -3169,7 +3354,7 @@ "name": "Compose Multiplatform Team" } ], - "artifactVersion": "1.1.0-alpha02", + "artifactVersion": "1.1.0-alpha03", "description": "Provides Bundle in Kotlin Multiplatform projects", "scm": { "connection": "scm:git:https://github.com/JetBrains/compose-jb.git", @@ -3191,7 +3376,7 @@ "name": "Compose Multiplatform Team" } ], - "artifactVersion": "1.1.0-alpha02", + "artifactVersion": "1.1.0-alpha03", "description": "Provides Uri in Kotlin Multiplatform projects", "scm": { "connection": "scm:git:https://github.com/JetBrains/compose-jb.git", @@ -3213,7 +3398,7 @@ "name": "Compose Multiplatform Team" } ], - "artifactVersion": "1.1.0-alpha02", + "artifactVersion": "1.1.0-alpha03", "description": "Provides Uri in Kotlin Multiplatform projects", "scm": { "connection": "scm:git:https://github.com/JetBrains/compose-jb.git", @@ -3235,7 +3420,7 @@ "name": "Compose Multiplatform Team" } ], - "artifactVersion": "1.7.0", + "artifactVersion": "1.8.0-alpha03", "description": "Resources for Compose JB", "scm": { "connection": "scm:git:https://github.com/JetBrains/compose-jb.git", @@ -3248,6 +3433,50 @@ "Apache-2.0" ] }, + { + "uniqueId": "org.jetbrains.compose.ui:ui-backhandler-android", + "funding": [], + "developers": [ + { + "organisationUrl": "https://www.jetbrains.com", + "name": "Compose Multiplatform Team" + } + ], + "artifactVersion": "1.8.0-alpha03", + "description": "Provides BackHandler in Compose Multiplatform projects", + "scm": { + "connection": "scm:git:https://github.com/JetBrains/compose-jb.git", + "url": "https://github.com/JetBrains/compose-jb", + "developerConnection": "scm:git:https://github.com/JetBrains/compose-jb.git" + }, + "name": "Compose Multiplatform BackHandler", + "website": "https://github.com/JetBrains/compose-jb", + "licenses": [ + "Apache-2.0" + ] + }, + { + "uniqueId": "org.jetbrains.compose.ui:ui-backhandler-android-debug", + "funding": [], + "developers": [ + { + "organisationUrl": "https://www.jetbrains.com", + "name": "Compose Multiplatform Team" + } + ], + "artifactVersion": "1.8.0-alpha03", + "description": "Provides BackHandler in Compose Multiplatform projects", + "scm": { + "connection": "scm:git:https://github.com/JetBrains/compose-jb.git", + "url": "https://github.com/JetBrains/compose-jb", + "developerConnection": "scm:git:https://github.com/JetBrains/compose-jb.git" + }, + "name": "Compose Multiplatform BackHandler", + "website": "https://github.com/JetBrains/compose-jb", + "licenses": [ + "Apache-2.0" + ] + }, { "uniqueId": "org.jetbrains.kotlin:kotlin-android-extensions-runtime", "funding": [], @@ -3624,6 +3853,51 @@ "Apache-2.0" ] }, + { + "uniqueId": "org.jsoup:jsoup", + "funding": [], + "developers": [ + { + "name": "Jonathan Hedley" + } + ], + "artifactVersion": "1.15.1", + "description": "jsoup is a Java library for working with real-world HTML. It provides a very convenient API for fetching URLs and extracting and manipulating data, using the best of HTML5 DOM methods and CSS selectors. jsoup implements the WHATWG HTML5 specification, and parses HTML to the same DOM as modern browsers do.", + "scm": { + "connection": "scm:git:https://github.com/jhy/jsoup.git", + "url": "https://github.com/jhy/jsoup" + }, + "name": "jsoup Java HTML Parser", + "website": "https://jsoup.org/", + "licenses": [ + "MIT" + ], + "organization": { + "url": "https://jhy.io/", + "name": "Jonathan Hedley" + } + }, + { + "uniqueId": "org.jspecify:jspecify", + "funding": [], + "developers": [ + { + "name": "Kevin Bourrillion" + } + ], + "artifactVersion": "1.0.0", + "description": "An artifact of well-named and well-specified annotations to power static analysis checks", + "scm": { + "connection": "scm:git:git@github.com:jspecify/jspecify.git", + "url": "https://github.com/jspecify/jspecify/", + "developerConnection": "scm:git:git@github.com:jspecify/jspecify.git" + }, + "name": "JSpecify annotations", + "website": "http://jspecify.org/", + "licenses": [ + "Apache-2.0" + ] + }, { "uniqueId": "org.slf4j:slf4j-api", "funding": [], @@ -3699,6 +3973,11 @@ "hash": "d8b9eb31a96ac01fd1bc3a7124e66dd3", "url": "https://developer.android.com/guide/playcore/license", "name": "Play Core Software Development Kit Terms of Service" + }, + "da5e524a4235731b9b96281090f5f82d": { + "hash": "da5e524a4235731b9b96281090f5f82d", + "url": "http://www.gnu.org/software/classpath/license.html", + "name": "GNU General Public License, version 2 (GPL2), with the classpath exception" } } } From 60b3eb1d6189dab4acf1147bf923e7385581dcda Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Thu, 20 Feb 2025 17:25:26 -0500 Subject: [PATCH 23/39] =?UTF-8?q?=E2=9A=96=EF=B8=8F=20Proper=20Modifier=20?= =?UTF-8?q?hoisting=20in=20the=20Home=20Screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Modifier parameter received from the platforms to be set in the HomeScreen was being ignored. --- .../com/escodro/home/presentation/HomeScreen.kt | 14 ++++++++++++-- .../com/escodro/shared/AlkaaMultiplatformApp.kt | 5 ++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt b/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt index 4cdb7c079..9b11ea4f4 100644 --- a/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt +++ b/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt @@ -52,13 +52,20 @@ import org.koin.compose.koinInject * Alkaa Home screen. */ @Composable -fun Home(appState: AppState) { - HomeLoader(appState = appState) +fun Home( + appState: AppState, + modifier: Modifier = Modifier, +) { + HomeLoader( + appState = appState, + modifier = modifier, + ) } @Composable private fun HomeLoader( appState: AppState, + modifier: Modifier = Modifier, navEventController: NavEventController = koinInject(), ) { val currentSection by appState.currentTopDestination @@ -73,6 +80,7 @@ private fun HomeLoader( navItems = navItems.toImmutableList(), currentSection = currentSection, setCurrentSection = setCurrentSection, + modifier = modifier, ) } @@ -82,6 +90,7 @@ private fun AlkaaHomeScaffold( navItems: ImmutableList, currentSection: TopLevel, setCurrentSection: (TopLevel) -> Unit, + modifier: Modifier = Modifier, ) { val showTopBar by appState.shouldShowTopAppBar.collectAsStateWithLifecycle(true) val topBarOffset: Dp by animateDpAsState( @@ -130,6 +139,7 @@ private fun AlkaaHomeScaffold( items = navItems, ) }, + modifier = modifier, ) } diff --git a/shared/src/commonMain/kotlin/com/escodro/shared/AlkaaMultiplatformApp.kt b/shared/src/commonMain/kotlin/com/escodro/shared/AlkaaMultiplatformApp.kt index 8168aa15e..a1769316c 100644 --- a/shared/src/commonMain/kotlin/com/escodro/shared/AlkaaMultiplatformApp.kt +++ b/shared/src/commonMain/kotlin/com/escodro/shared/AlkaaMultiplatformApp.kt @@ -22,7 +22,10 @@ fun AlkaaMultiplatformApp( val isDarkTheme = rememberIsDarkTheme() onThemeUpdate(isDarkTheme) AlkaaTheme(isDarkTheme = isDarkTheme) { - Home(appState = appState) + Home( + appState = appState, + modifier = modifier, + ) } } From 4e2ddd55f99d8434c76bac1ed18a966a20884d19 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sat, 22 Feb 2025 10:13:24 -0500 Subject: [PATCH 24/39] =?UTF-8?q?=F0=9F=A7=A3=20Implement=20`NavigationSui?= =?UTF-8?q?teScaffold`=20in=20Home=20Screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `NavigationSuiteScaffold` component is now used to handle the bottom navigation, allowing the app to adapt the navigation UI based on the screen size and automatically switch between navigation bar and rail. The `Scaffold` component is now used inside the new component. --- features/home/build.gradle.kts | 1 + .../escodro/home/presentation/HomeScreen.kt | 122 +++++++++--------- .../com/escodro/alkaa/HomeScreenTest.kt | 8 +- 3 files changed, 63 insertions(+), 68 deletions(-) diff --git a/features/home/build.gradle.kts b/features/home/build.gradle.kts index f23ca3b63..707e0b83e 100644 --- a/features/home/build.gradle.kts +++ b/features/home/build.gradle.kts @@ -29,6 +29,7 @@ kotlin { implementation(compose.material) implementation(compose.material3) implementation(compose.components.resources) + implementation(compose.material3AdaptiveNavigationSuite) implementation(libs.koin.compose.jb) implementation(libs.kotlinx.collections.immutable) diff --git a/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt b/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt index 9b11ea4f4..a6bee9b91 100644 --- a/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt +++ b/features/home/src/commonMain/kotlin/com/escodro/home/presentation/HomeScreen.kt @@ -4,8 +4,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.calculateEndPadding @@ -16,20 +14,21 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material3.BottomAppBar import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -97,50 +96,47 @@ private fun AlkaaHomeScaffold( targetValue = if (showTopBar) 0.dp else 64.dp, animationSpec = tween(easing = LinearEasing), ) - Scaffold( - topBar = { - AnimatedVisibility( - visible = showTopBar, - enter = TopBarEnterTransition, - exit = TopBarExitTransition, - ) { - AlkaaTopBar(currentSection = currentSection) - } - }, - contentWindowInsets = WindowInsets(0, 0, 0, 0), - content = { paddingValues -> - val topPadding = paddingValues.calculateTopPadding() - topBarOffset - Row( - Modifier - .fillMaxSize() - .padding( - start = paddingValues.calculateStartPadding(LocalLayoutDirection.current), - top = if (topPadding > 0.dp) topPadding else 0.dp, - end = paddingValues.calculateEndPadding(LocalLayoutDirection.current), - bottom = paddingValues.calculateBottomPadding(), - ).consumeWindowInsets(paddingValues) - .windowInsetsPadding( - WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal), - ), - ) { - // TODO implement with NavigationSuiteScaffold - Column(Modifier.fillMaxSize()) { - Navigation( - startDestination = HomeDestination.TaskList, - navHostController = appState.navHostController, - ) - } - } - }, - bottomBar = { - AlkaaBottomNav( - currentSection = currentSection, - onSectionSelect = setCurrentSection, + NavigationSuiteScaffold( + navigationSuiteItems = { + alkaaBottomNav( items = navItems, + currentSection = currentSection, + setCurrentSection = setCurrentSection, ) }, modifier = modifier, - ) + ) { + Scaffold( + topBar = { + AnimatedVisibility( + visible = showTopBar, + enter = TopBarEnterTransition, + exit = TopBarExitTransition, + ) { + AlkaaTopBar(currentSection = currentSection) + } + }, + contentWindowInsets = WindowInsets(0, 0, 0, 0), + content = { paddingValues -> + val topPadding = paddingValues.calculateTopPadding() - topBarOffset + Navigation( + startDestination = HomeDestination.TaskList, + navHostController = appState.navHostController, + modifier = Modifier + .fillMaxSize() + .padding( + start = paddingValues.calculateStartPadding(LocalLayoutDirection.current), + top = if (topPadding > 0.dp) topPadding else 0.dp, + end = paddingValues.calculateEndPadding(LocalLayoutDirection.current), + bottom = paddingValues.calculateBottomPadding(), + ).consumeWindowInsets(paddingValues) + .windowInsetsPadding( + WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal), + ), + ) + }, + ) + } } @OptIn(ExperimentalMaterial3Api::class) @@ -157,27 +153,25 @@ private fun AlkaaTopBar(currentSection: TopLevel) { ) } -@Composable -private fun AlkaaBottomNav( - currentSection: TopLevel, - onSectionSelect: (TopLevel) -> Unit, +private fun NavigationSuiteScope.alkaaBottomNav( items: ImmutableList, + currentSection: TopLevel, + setCurrentSection: (TopLevel) -> Unit, ) { - BottomAppBar(containerColor = MaterialTheme.colorScheme.background) { - items.forEach { section -> - val selected = section == currentSection - val title = section.title - NavigationBarItem( - selected = selected, - onClick = { onSectionSelect(section) }, - icon = { - Icon( - imageVector = section.icon, - contentDescription = stringResource(title), - ) - }, - label = { Text(stringResource(title)) }, - ) - } + items.forEach { section -> + val selected = section == currentSection + val title = section.title + item( + selected = selected, + onClick = { setCurrentSection(section) }, + icon = { + Icon( + imageVector = section.icon, + contentDescription = stringResource(title), + ) + }, + label = { Text(stringResource(title)) }, + modifier = Modifier.testTag(title.toString()), + ) } } diff --git a/shared/src/commonTest/kotlin/com/escodro/alkaa/HomeScreenTest.kt b/shared/src/commonTest/kotlin/com/escodro/alkaa/HomeScreenTest.kt index 3c7c4217c..174881ced 100644 --- a/shared/src/commonTest/kotlin/com/escodro/alkaa/HomeScreenTest.kt +++ b/shared/src/commonTest/kotlin/com/escodro/alkaa/HomeScreenTest.kt @@ -4,8 +4,8 @@ import androidx.compose.ui.input.key.Key import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.isRoot -import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performKeyInput import com.escodro.alkaa.test.afterTest @@ -39,7 +39,7 @@ internal class HomeScreenTest : KoinTest { // Click on each item and validate the title onNodeWithContentDescription(label = title, useUnmergedTree = true).performClick() - onAllNodesWithText(title)[1].assertIsSelected() + onNodeWithTag(section.title.toString()).assertIsSelected() } } @@ -48,7 +48,7 @@ internal class HomeScreenTest : KoinTest { // Click on Settings tab val settingsTitle = runBlocking { getString(HomeSection.Settings.title) } onNodeWithContentDescription(label = settingsTitle, useUnmergedTree = true).performClick() - onAllNodesWithText(settingsTitle)[1].assertIsSelected() + onNodeWithTag(HomeSection.Settings.title.toString()).assertIsSelected() // Press back button onAllNodes(isRoot())[0].performKeyInput { @@ -59,6 +59,6 @@ internal class HomeScreenTest : KoinTest { // Click on Tasks tab val tasksTitle = runBlocking { getString(HomeSection.Tasks.title) } onNodeWithContentDescription(label = tasksTitle, useUnmergedTree = true).performClick() - onAllNodesWithText(tasksTitle)[1].assertIsSelected() + onNodeWithTag(HomeSection.Tasks.title.toString()).assertIsSelected() } } From ef059a1c930e6623edd84601a29640af5ee69ca5 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sun, 23 Feb 2025 12:59:41 -0500 Subject: [PATCH 25/39] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Update=20ProGuard?= =?UTF-8?q?=20rules=20for=20androidx.window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated ProGuard rules to keep and ignore warnings related to the `androidx.window` library. Specifically, this change ensures that classes within `androidx.window` are not obfuscated and suppresses warnings related to the same. --- app/proguard-rules.pro | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 09f714c28..9897a105c 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -28,4 +28,6 @@ -keepnames class * implements android.os.Parcelable* -dontwarn javax.servlet.ServletContainerInitializer --dontwarn androidx.window.extensions.area.ExtensionWindowAreaPresentation + +-keep class androidx.window.** { *; } + -dontwarn androidx.window.** From c8ef059da5ef0671d8bbc173b59306fc0b5d0b47 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Thu, 13 Mar 2025 19:56:15 -0400 Subject: [PATCH 26/39] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20task=20layout?= =?UTF-8?q?=20and=20integrate=20adaptive=20navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated task detail and list views for improved UI scrolling and navigation. Integrated Compose Adaptive Navigation library to implement a three-pane scaffold for better task management flow. Simplified task filtering and category handling for easier maintenance. --- features/task/build.gradle.kts | 1 + .../escodro/task/navigation/TaskNavGraph.kt | 3 +- .../presentation/detail/main/TaskDetail.kt | 6 +- .../task/presentation/list/TaskList.kt | 180 +++++++++++------- gradle/libs.versions.toml | 3 + .../values-pt-rBR/strings.xml | 2 + .../composeResources/values/strings.xml | 2 + 7 files changed, 127 insertions(+), 70 deletions(-) diff --git a/features/task/build.gradle.kts b/features/task/build.gradle.kts index 582205c19..c1a9d9df7 100644 --- a/features/task/build.gradle.kts +++ b/features/task/build.gradle.kts @@ -27,6 +27,7 @@ kotlin { implementation(compose.material3) implementation(compose.materialIconsExtended) implementation(compose.components.resources) + implementation(libs.compose.adaptive.navigation) implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.collections.immutable) diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt b/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt index 62a769d26..0bb33ee94 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt @@ -23,8 +23,7 @@ internal class TaskNavGraph : NavGraph { override val navGraph: NavGraphBuilder.(NavEventController) -> Unit = { navEventController -> composable { TaskListSection( - onItemClick = { id -> navEventController.sendEvent(TaskEvent.OnTaskClick(id)) }, - onAddClick = { navEventController.sendEvent(TaskEvent.OnNewTaskClick) }, + onFabClick = { navEventController.sendEvent(TaskEvent.OnNewTaskClick) }, ) } diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt index e18fec12c..8528ffe6b 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt @@ -4,8 +4,10 @@ import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.outlined.Bookmark @@ -20,7 +22,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.input.ImeAction @@ -121,7 +122,7 @@ private fun TaskDetailContent( actions: TaskDetailActions, ) { Surface(color = MaterialTheme.colorScheme.background) { - Column { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { TaskTitleTextField(text = task.title, onTitleChange = actions.onTitleChange) TaskDetailSectionContent( imageVector = Icons.Outlined.Bookmark, @@ -159,7 +160,6 @@ private fun TaskDetailError() { ) } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun TaskTitleTextField(text: String, onTitleChange: (String) -> Unit) { val textState = remember { mutableStateOf(TextFieldValue(text)) } diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt index ddb8675d2..4034cef5f 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt @@ -1,13 +1,13 @@ package com.escodro.task.presentation.list import androidx.compose.animation.Crossfade -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.ThumbUp import androidx.compose.material3.FabPosition @@ -16,6 +16,12 @@ import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -35,6 +41,7 @@ import com.escodro.designsystem.components.AlkaaLoadingContent import com.escodro.designsystem.components.DefaultIconTextContent import com.escodro.resources.Res import com.escodro.resources.task_cd_add_task +import com.escodro.resources.task_detail_pane_title import com.escodro.resources.task_list_cd_empty_list import com.escodro.resources.task_list_cd_error import com.escodro.resources.task_list_header_empty @@ -44,6 +51,8 @@ import com.escodro.resources.task_snackbar_message_complete import com.escodro.task.model.TaskWithCategory import com.escodro.task.presentation.category.CategorySelection import com.escodro.task.presentation.detail.main.CategoryId +import com.escodro.task.presentation.detail.main.TaskDetailScreen +import com.escodro.task.presentation.detail.main.TaskId import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -53,30 +62,29 @@ import org.koin.compose.koinInject /** * Alkaa Task Section. * + * @param onFabClick the action to be executed when the add button is clicked * @param modifier the decorator */ @Composable fun TaskListSection( - onItemClick: (Long) -> Unit, - onAddClick: () -> Unit, + onFabClick: () -> Unit, modifier: Modifier = Modifier, ) { TaskListLoader( modifier = modifier, - onItemClick = onItemClick, - onAddClick = onAddClick, + onFabClick = onFabClick, ) } +@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun TaskListLoader( - onItemClick: (Long) -> Unit, - onAddClick: () -> Unit, + onFabClick: () -> Unit, modifier: Modifier = Modifier, taskListViewModel: TaskListViewModel = koinInject(), categoryViewModel: CategoryListViewModel = koinInject(), ) { - val (currentCategory, setCategory) = rememberSaveable { mutableStateOf(null) } + val (currentCategory, onCategoryChange) = rememberSaveable { mutableStateOf(null) } // val refreshKey = rememberRefreshKey() https://github.com/JetBrains/compose-multiplatform/issues/4805 val taskViewState by remember(taskListViewModel, currentCategory) { @@ -87,30 +95,65 @@ internal fun TaskListLoader( categoryViewModel.loadCategories() }.collectAsState(initial = CategoryState.Loading) - val taskHandler = TaskStateHandler( - state = taskViewState, - onCheckedChange = taskListViewModel::updateTaskStatus, - onItemClick = onItemClick, - onAddClick = onAddClick, - ) - - val categoryHandler = CategoryStateHandler( - state = categoryViewState, - currentCategory = currentCategory, - onCategoryChange = setCategory, - ) + val navigator: ThreePaneScaffoldNavigator = + rememberListDetailPaneScaffoldNavigator() + val coroutineScope = rememberCoroutineScope() - TaskListScaffold( - taskHandler = taskHandler, - categoryHandler = categoryHandler, - modifier = modifier, + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + TaskListScaffold( + taskViewState = taskViewState, + categoryViewState = categoryViewState, + onTaskCheckedChange = { item -> + taskListViewModel.updateTaskStatus(item) + coroutineScope.launch { navigator.navigateBack() } + }, + onFabClick = onFabClick, + currentCategory = currentCategory, + onCategoryChange = onCategoryChange, + modifier = modifier, + onItemClick = { + coroutineScope.launch { + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, TaskId(it)) + } + }, + ) + } + }, + detailPane = { + AnimatedPane { + val taskId = navigator.currentDestination?.contentKey?.value + if (taskId != null) { + TaskDetailScreen( + taskId = taskId, + onUpPress = { + coroutineScope.launch { navigator.navigateBack() } + }, + ) + } else { + DefaultIconTextContent( + icon = Icons.Outlined.CheckCircle, + iconContentDescription = stringResource(Res.string.task_list_cd_error), + header = stringResource(Res.string.task_detail_pane_title), + ) + } + } + }, ) } @Composable internal fun TaskListScaffold( - taskHandler: TaskStateHandler, - categoryHandler: CategoryStateHandler, + taskViewState: TaskListViewState, + categoryViewState: CategoryState, + onFabClick: () -> Unit, + onTaskCheckedChange: (TaskWithCategory) -> Unit, + onItemClick: (Long) -> Unit, + currentCategory: CategoryId?, + onCategoryChange: (CategoryId?) -> Unit, modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() @@ -128,57 +171,64 @@ internal fun TaskListScaffold( ) when (snackbarResult) { SnackbarResult.Dismissed -> {} // Do nothing - SnackbarResult.ActionPerformed -> taskHandler.onCheckedChange(taskWithCategory) + SnackbarResult.ActionPerformed -> onTaskCheckedChange(taskWithCategory) } } } - BoxWithConstraints { - val fabPosition = if (this.maxHeight > maxWidth) FabPosition.Center else FabPosition.End - Scaffold( - modifier = modifier.fillMaxSize(), - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - topBar = { TaskFilter(categoryHandler = categoryHandler) }, - floatingActionButton = { - AddFloatingButton( - contentDescription = stringResource(Res.string.task_cd_add_task), - onClick = { taskHandler.onAddClick() }, - ) - }, - floatingActionButtonPosition = fabPosition, - ) { paddingValues -> - Crossfade( - targetState = taskHandler.state, - modifier = Modifier.padding(paddingValues), - ) { state -> - when (state) { - TaskListViewState.Loading -> AlkaaLoadingContent() - is TaskListViewState.Error -> TaskListError() - is TaskListViewState.Loaded -> { - val taskList = state.items - TaskListContent( - taskList = taskList, - onItemClick = taskHandler.onItemClick, - onCheckedChange = { taskWithCategory -> - taskHandler.onCheckedChange(taskWithCategory) - onShowSnackbar(taskWithCategory) - }, - ) - } - - TaskListViewState.Empty -> TaskListEmpty() + Scaffold( + modifier = modifier.fillMaxSize(), + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + topBar = { + TaskFilter( + categoryState = categoryViewState, + currentCategory = currentCategory, + onCategoryChange = onCategoryChange, + ) + }, + floatingActionButton = { + AddFloatingButton( + contentDescription = stringResource(Res.string.task_cd_add_task), + onClick = onFabClick, + ) + }, + floatingActionButtonPosition = FabPosition.Center, + ) { paddingValues -> + Crossfade( + targetState = taskViewState, + modifier = Modifier.padding(paddingValues), + ) { state -> + when (state) { + TaskListViewState.Loading -> AlkaaLoadingContent() + is TaskListViewState.Error -> TaskListError() + is TaskListViewState.Loaded -> { + val taskList = state.items + TaskListContent( + taskList = taskList, + onItemClick = onItemClick, + onCheckedChange = { taskWithCategory -> + onTaskCheckedChange(taskWithCategory) + onShowSnackbar(taskWithCategory) + }, + ) } + + TaskListViewState.Empty -> TaskListEmpty() } } } } @Composable -private fun TaskFilter(categoryHandler: CategoryStateHandler) { +private fun TaskFilter( + categoryState: CategoryState, + currentCategory: CategoryId?, + onCategoryChange: (CategoryId?) -> Unit, +) { CategorySelection( - state = categoryHandler.state, - currentCategory = categoryHandler.currentCategory?.value, - onCategoryChange = categoryHandler.onCategoryChange, + state = categoryState, + currentCategory = currentCategory?.value, + onCategoryChange = onCategoryChange, modifier = Modifier.padding(start = 16.dp), ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 168f48b9c..2cc0b9aed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,6 +40,7 @@ compose_bom = "2025.03.00" compose_activity = "1.10.1" compose_compiler = "1.8.0-alpha03" compose_navigation = "2.8.0-alpha13" +compose_adaptive = "1.1.0-alpha04" # Koin koin_core = "4.0.2" @@ -103,6 +104,8 @@ compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = compose_ui = { module = "androidx.compose.ui:ui" } compose_icons = { module = "androidx.compose.material:material-icons-extended" } compose_material3 = { module = "androidx.compose.material3:material3" } +compose_adaptive_navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "compose_adaptive" } + compose_navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "compose_navigation" } # Dynamic Feature don't recognize BOM diff --git a/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml b/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml index 872e6af71..cfe330d0a 100644 --- a/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -24,6 +24,8 @@ Sem alarme + Selecione uma tarefa para ver os detalhes + Remover alarme Nunca diff --git a/resources/src/commonMain/composeResources/values/strings.xml b/resources/src/commonMain/composeResources/values/strings.xml index 866b9b681..ab1b86364 100644 --- a/resources/src/commonMain/composeResources/values/strings.xml +++ b/resources/src/commonMain/composeResources/values/strings.xml @@ -26,6 +26,8 @@ No alarm + Select a task to see the details + Never Every hour Every day From 29e17209a1cd0f16cdcb14fb8613f7ae37833f1e Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Thu, 13 Mar 2025 20:03:39 -0400 Subject: [PATCH 27/39] =?UTF-8?q?=E2=9C=A8=20Update=20Compose=20Compiler?= =?UTF-8?q?=20to=20version=201.8.0-alpha04?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgraded the Compose Compiler dependency to the latest alpha version. This ensures compatibility with recent Compose features and includes potential performance improvements or bug fixes. --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2cc0b9aed..dcb708d5e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,7 +38,7 @@ annotation = "1.9.1" # Compose compose_bom = "2025.03.00" compose_activity = "1.10.1" -compose_compiler = "1.8.0-alpha03" +compose_compiler = "1.8.0-alpha04" compose_navigation = "2.8.0-alpha13" compose_adaptive = "1.1.0-alpha04" From e094c1abe588ff0c116eb70e12a6f3e64b6c8487 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Fri, 14 Mar 2025 15:51:47 -0400 Subject: [PATCH 28/39] =?UTF-8?q?=E2=9C=85=20Update=20tests=20to=20run=20b?= =?UTF-8?q?etter=20on=20dynamic=20screens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The iOS tests runs on a dynamic window setup, meaning that it might show the multiple panes, with multiple components with the same text or role. The tests were updated to make the tests more permissive, allowing better dynamic support. --- .../kotlin/com/escodro/alkaa/TaskFlowTest.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/shared/src/commonTest/kotlin/com/escodro/alkaa/TaskFlowTest.kt b/shared/src/commonTest/kotlin/com/escodro/alkaa/TaskFlowTest.kt index b363d5713..87066caa4 100644 --- a/shared/src/commonTest/kotlin/com/escodro/alkaa/TaskFlowTest.kt +++ b/shared/src/commonTest/kotlin/com/escodro/alkaa/TaskFlowTest.kt @@ -2,9 +2,11 @@ package com.escodro.alkaa import androidx.compose.ui.test.ComposeUiTest import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertAny import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.hasSetTextAction import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.onLast import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -121,13 +123,13 @@ internal class TaskFlowTest : KoinTest { // Select a category val category = "Music" - onNodeWithText(category).performClick() + onAllNodes(hasText(category)).onLast().performClick() onNodeWithContentDescription("Back", useUnmergedTree = true).performClick() // Reopen the task and validate if the category is selected waitUntilAtLeastOneExists(hasText(taskName)) onNodeWithText(text = taskName, useUnmergedTree = true).performClick() - onNodeWithText(category).assertIsSelected() + onAllNodes(hasText(category)).onLast().assertIsSelected() } @Test @@ -186,10 +188,9 @@ internal class TaskFlowTest : KoinTest { onNodeWithText("Add").performClick() // Open and validate - waitUntilExactlyOneExists(hasText(taskName)) + waitUntilAtLeastOneExists(hasText(taskName)) onNodeWithText(text = taskName, useUnmergedTree = true).performClick() - waitUntilExactlyOneExists(hasText(taskName)) - onNodeWithText(text = taskName, useUnmergedTree = true).assertExists() + waitUntilAtLeastOneExists(hasText(taskName)) onNodeWithText("No alarm", useUnmergedTree = true).assertDoesNotExist() onNodeWithText("Every day", useUnmergedTree = true).assertExists() } @@ -201,7 +202,7 @@ internal class TaskFlowTest : KoinTest { onNodeWithText("Add").performClick() waitUntilExactlyOneExists(hasText(taskName)) onNodeWithText(text = taskName, useUnmergedTree = true).performClick() - waitUntilExactlyOneExists(hasText(taskName)) - onNodeWithText(text = taskName, useUnmergedTree = true).assertExists() + waitUntilAtLeastOneExists(hasText(taskName)) + onAllNodes(hasSetTextAction()).assertAny(hasText(taskName, substring = true)) } } From 781c644315d17b25438da755ed26c441fa67b934 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sat, 15 Mar 2025 19:48:47 -0400 Subject: [PATCH 29/39] =?UTF-8?q?=F0=9F=9A=A7=20Work=20in=20Progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../escodro/task/navigation/TaskNavGraph.kt | 11 +++++ .../task/presentation/list/TaskList.kt | 44 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt b/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt index 0bb33ee94..bcea0eb68 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt @@ -1,10 +1,14 @@ package com.escodro.task.navigation +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.compose.dialog import androidx.navigation.navDeepLink import androidx.navigation.toRoute +import androidx.window.core.layout.WindowHeightSizeClass +import androidx.window.core.layout.WindowSizeClass +import androidx.window.core.layout.WindowWidthSizeClass import com.escodro.designsystem.animation.SlideInHorizontallyTransition import com.escodro.designsystem.animation.SlideOutHorizontallyTransition import com.escodro.navigationapi.controller.NavEventController @@ -22,7 +26,10 @@ internal class TaskNavGraph : NavGraph { override val navGraph: NavGraphBuilder.(NavEventController) -> Unit = { navEventController -> composable { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass TaskListSection( + isCompact = windowSizeClass.isCompact(), + onItemClick = { taskId -> navEventController.sendEvent(TaskEvent.OnTaskClick(taskId)) }, onFabClick = { navEventController.sendEvent(TaskEvent.OnNewTaskClick) }, ) } @@ -50,3 +57,7 @@ internal class TaskNavGraph : NavGraph { } } } + +private fun WindowSizeClass.isCompact(): Boolean = + windowWidthSizeClass == WindowWidthSizeClass.COMPACT || + windowHeightSizeClass == WindowHeightSizeClass.COMPACT diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt index 4034cef5f..ab674e555 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt @@ -67,18 +67,23 @@ import org.koin.compose.koinInject */ @Composable fun TaskListSection( + isCompact: Boolean, + onItemClick: (Long) -> Unit, onFabClick: () -> Unit, modifier: Modifier = Modifier, ) { TaskListLoader( + isCompact = isCompact, modifier = modifier, onFabClick = onFabClick, + onItemClick = onItemClick, ) } -@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun TaskListLoader( + isCompact: Boolean, + onItemClick: (Long) -> Unit, onFabClick: () -> Unit, modifier: Modifier = Modifier, taskListViewModel: TaskListViewModel = koinInject(), @@ -95,6 +100,41 @@ internal fun TaskListLoader( categoryViewModel.loadCategories() }.collectAsState(initial = CategoryState.Loading) + if (isCompact) { + TaskListScaffold( + taskViewState = taskViewState, + categoryViewState = categoryViewState, + onTaskCheckedChange = taskListViewModel::updateTaskStatus, + onFabClick = onFabClick, + currentCategory = currentCategory, + onCategoryChange = onCategoryChange, + modifier = modifier, + onItemClick = onItemClick, + ) + } else { + AdaptiveTaskListScaffold( + taskViewState = taskViewState, + categoryViewState = categoryViewState, + onUpdateTaskStatus = taskListViewModel::updateTaskStatus, + onFabClick = onFabClick, + currentCategory = currentCategory, + onCategoryChange = onCategoryChange, + modifier = modifier, + ) + } +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +private fun AdaptiveTaskListScaffold( + taskViewState: TaskListViewState, + categoryViewState: CategoryState, + onUpdateTaskStatus: (TaskWithCategory) -> Unit, + onFabClick: () -> Unit, + currentCategory: CategoryId?, + onCategoryChange: (CategoryId?) -> Unit, + modifier: Modifier = Modifier, +) { val navigator: ThreePaneScaffoldNavigator = rememberListDetailPaneScaffoldNavigator() val coroutineScope = rememberCoroutineScope() @@ -108,7 +148,7 @@ internal fun TaskListLoader( taskViewState = taskViewState, categoryViewState = categoryViewState, onTaskCheckedChange = { item -> - taskListViewModel.updateTaskStatus(item) + onUpdateTaskStatus(item) coroutineScope.launch { navigator.navigateBack() } }, onFabClick = onFabClick, From 6d896a1bcbbdd0729515c25550893a7b68d006b5 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sun, 16 Mar 2025 11:41:36 -0400 Subject: [PATCH 30/39] =?UTF-8?q?=E2=9C=A8=20Refactor=20WindowSizeClass=20?= =?UTF-8?q?extension=20and=20dependency=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved `isCompact` function to `navigationapi` module for clearer separation of concerns. Removed unused `adaptive.navigation` dependency from the `task` module and added it to `navigationapi` for proper modularization. --- features/navigation-api/build.gradle.kts | 2 ++ .../extension/WindowSizeClassExtension.kt | 14 ++++++++++++++ features/task/build.gradle.kts | 1 - .../com/escodro/task/navigation/TaskNavGraph.kt | 8 +------- 4 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt diff --git a/features/navigation-api/build.gradle.kts b/features/navigation-api/build.gradle.kts index 4b45036eb..76ab0831b 100644 --- a/features/navigation-api/build.gradle.kts +++ b/features/navigation-api/build.gradle.kts @@ -14,6 +14,8 @@ kotlin { sourceSets { commonMain.dependencies { api(libs.compose.navigation) + api(libs.compose.adaptive.navigation) + implementation(projects.resources) implementation(projects.libraries.parcelable) diff --git a/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt b/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt new file mode 100644 index 000000000..4b654fbf8 --- /dev/null +++ b/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt @@ -0,0 +1,14 @@ +package com.escodro.navigationapi.extension + +import androidx.window.core.layout.WindowHeightSizeClass +import androidx.window.core.layout.WindowSizeClass +import androidx.window.core.layout.WindowWidthSizeClass + +/** + * Check if the [WindowSizeClass] is compact. + * + * @return true if the [WindowSizeClass] is compact, false otherwise + */ +fun WindowSizeClass.isCompact(): Boolean = + windowWidthSizeClass == WindowWidthSizeClass.COMPACT || + windowHeightSizeClass == WindowHeightSizeClass.COMPACT diff --git a/features/task/build.gradle.kts b/features/task/build.gradle.kts index c1a9d9df7..582205c19 100644 --- a/features/task/build.gradle.kts +++ b/features/task/build.gradle.kts @@ -27,7 +27,6 @@ kotlin { implementation(compose.material3) implementation(compose.materialIconsExtended) implementation(compose.components.resources) - implementation(libs.compose.adaptive.navigation) implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.collections.immutable) diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt b/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt index bcea0eb68..a28249982 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt @@ -6,9 +6,6 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.dialog import androidx.navigation.navDeepLink import androidx.navigation.toRoute -import androidx.window.core.layout.WindowHeightSizeClass -import androidx.window.core.layout.WindowSizeClass -import androidx.window.core.layout.WindowWidthSizeClass import com.escodro.designsystem.animation.SlideInHorizontallyTransition import com.escodro.designsystem.animation.SlideOutHorizontallyTransition import com.escodro.navigationapi.controller.NavEventController @@ -17,6 +14,7 @@ import com.escodro.navigationapi.destination.HomeDestination import com.escodro.navigationapi.destination.TasksDestination import com.escodro.navigationapi.event.Event import com.escodro.navigationapi.event.TaskEvent +import com.escodro.navigationapi.extension.isCompact import com.escodro.navigationapi.provider.NavGraph import com.escodro.task.presentation.add.AddTaskBottomSheet import com.escodro.task.presentation.detail.main.TaskDetailScreen @@ -57,7 +55,3 @@ internal class TaskNavGraph : NavGraph { } } } - -private fun WindowSizeClass.isCompact(): Boolean = - windowWidthSizeClass == WindowWidthSizeClass.COMPACT || - windowHeightSizeClass == WindowHeightSizeClass.COMPACT From 0531e33dce7bea18f1d35d08068ef25959fae072 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sun, 16 Mar 2025 11:51:33 -0400 Subject: [PATCH 31/39] =?UTF-8?q?=E2=9C=A8=20Add=20adaptive=20UI=20for=20S?= =?UTF-8?q?earch=20with=20compact=20and=20detail=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced adaptive UI logic to support both compact and detail views in the Search feature. Compact mode uses the existing SearchScaffold, while larger screens utilize a new AdaptiveSearchScaffold with a List-Detail pane navigation system. This improves usability across different screen sizes and enhances user experience. Next commits will connect the Search with the Task Details Composable --- .../search/navigation/SearchNavGraph.kt | 4 + .../com/escodro/search/presentation/Search.kt | 93 +++++++++++++++++-- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/features/search/src/commonMain/kotlin/com/escodro/search/navigation/SearchNavGraph.kt b/features/search/src/commonMain/kotlin/com/escodro/search/navigation/SearchNavGraph.kt index 55e9b5f9c..5bad806bf 100644 --- a/features/search/src/commonMain/kotlin/com/escodro/search/navigation/SearchNavGraph.kt +++ b/features/search/src/commonMain/kotlin/com/escodro/search/navigation/SearchNavGraph.kt @@ -1,10 +1,12 @@ package com.escodro.search.navigation +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.escodro.navigationapi.controller.NavEventController import com.escodro.navigationapi.destination.HomeDestination import com.escodro.navigationapi.event.TaskEvent +import com.escodro.navigationapi.extension.isCompact import com.escodro.navigationapi.provider.NavGraph import com.escodro.search.presentation.SearchSection @@ -12,7 +14,9 @@ internal class SearchNavGraph : NavGraph { override val navGraph: NavGraphBuilder.(NavEventController) -> Unit = { navEventController -> composable { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass SearchSection( + isCompact = windowSizeClass.isCompact(), onItemClick = { navEventController.sendEvent(TaskEvent.OnTaskClick(id = it)) }, ) } diff --git a/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt b/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt index 54414cbd3..94b1f3470 100644 --- a/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt +++ b/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt @@ -20,16 +20,24 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ExitToApp import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextField +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -45,8 +53,11 @@ import com.escodro.resources.Res import com.escodro.resources.search_cd_empty_list import com.escodro.resources.search_cd_icon import com.escodro.resources.search_header_empty +import com.escodro.resources.task_detail_pane_title +import com.escodro.resources.task_list_cd_error import com.escodro.search.model.TaskSearchItem import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject @@ -57,14 +68,20 @@ import org.koin.compose.koinInject */ @Composable fun SearchSection( + isCompact: Boolean, onItemClick: (Long) -> Unit, modifier: Modifier = Modifier, ) { - SearchLoader(modifier = modifier, onItemClick = onItemClick) + SearchLoader( + isCompact = isCompact, + modifier = modifier, + onItemClick = onItemClick, + ) } @Composable private fun SearchLoader( + isCompact: Boolean, onItemClick: (Long) -> Unit, modifier: Modifier = Modifier, viewModel: SearchViewModel = koinInject(), @@ -74,12 +91,74 @@ private fun SearchLoader( viewModel.findTasksByName(query) }.collectAsState(initial = SearchViewState.Loading) - SearchScaffold( - viewState = viewState, - modifier = modifier, - onItemClick = onItemClick, - query = query, - setQuery = setQuery, + if (isCompact) { + SearchScaffold( + viewState = viewState, + modifier = modifier, + onItemClick = onItemClick, + query = query, + setQuery = setQuery, + ) + } else { + AdaptiveSearchScaffold( + viewState = viewState, + query = query, + setQuery = setQuery, + modifier = modifier, + ) + } +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +private fun AdaptiveSearchScaffold( + viewState: SearchViewState, + query: String, + setQuery: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val navigator: ThreePaneScaffoldNavigator = + rememberListDetailPaneScaffoldNavigator() + val coroutineScope = rememberCoroutineScope() + + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + SearchScaffold( + viewState = viewState, + modifier = modifier, + onItemClick = { + coroutineScope.launch { + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, it) + } + }, + query = query, + setQuery = setQuery, + ) + } + }, + detailPane = { + AnimatedPane { + val taskId = navigator.currentDestination?.contentKey + if (taskId != null) { +// TODO connect with the composable +// TaskDetailScreen( +// taskId = taskId, +// onUpPress = { +// coroutineScope.launch { navigator.navigateBack() } +// }, +// ) + } else { + DefaultIconTextContent( + icon = Icons.Outlined.CheckCircle, + iconContentDescription = stringResource(Res.string.task_list_cd_error), + header = stringResource(Res.string.task_detail_pane_title), + ) + } + } + }, ) } From e004844ae4168bfbbba77419bb2910dce6555482 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sun, 16 Mar 2025 12:07:01 -0400 Subject: [PATCH 32/39] =?UTF-8?q?=F0=9F=93=BF=20Add=20Task=20API=20module?= =?UTF-8?q?=20and=20integrate=20TaskDetailScreen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the `task-api` module to provide an interface for task details. Refactors the `Search` and `Task` features to utilize the `TaskDetailScreen` interface for a more modular and composable architecture. Updates dependency configurations and injects the implementation using Koin. --- features/search/build.gradle.kts | 1 + .../com/escodro/search/presentation/Search.kt | 21 ++++++++++++------ features/task-api/.gitignore | 1 + features/task-api/build.gradle.kts | 22 +++++++++++++++++++ .../com/escodro/taskapi/TaskDetailScreen.kt | 9 ++++++++ features/task/build.gradle.kts | 1 + .../kotlin/com/escodro/task/di/TaskModule.kt | 4 ++++ .../detail/main/TaskDetailScreenImpl.kt | 15 +++++++++++++ settings.gradle.kts | 1 + 9 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 features/task-api/.gitignore create mode 100644 features/task-api/build.gradle.kts create mode 100644 features/task-api/src/commonMain/kotlin/com/escodro/taskapi/TaskDetailScreen.kt create mode 100644 features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt diff --git a/features/search/build.gradle.kts b/features/search/build.gradle.kts index 1c7094d0b..0053727fc 100644 --- a/features/search/build.gradle.kts +++ b/features/search/build.gradle.kts @@ -15,6 +15,7 @@ kotlin { implementation(projects.libraries.designsystem) implementation(projects.resources) implementation(projects.features.navigationApi) + implementation(projects.features.taskApi) implementation(projects.libraries.di) implementation(compose.runtime) diff --git a/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt b/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt index 94b1f3470..184b39b1e 100644 --- a/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt +++ b/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt @@ -56,6 +56,7 @@ import com.escodro.resources.search_header_empty import com.escodro.resources.task_detail_pane_title import com.escodro.resources.task_list_cd_error import com.escodro.search.model.TaskSearchItem +import com.escodro.taskapi.TaskDetailScreen import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource @@ -85,6 +86,7 @@ private fun SearchLoader( onItemClick: (Long) -> Unit, modifier: Modifier = Modifier, viewModel: SearchViewModel = koinInject(), + taskDetailScreen: TaskDetailScreen = koinInject(), ) { val (query, setQuery) = rememberSaveable { mutableStateOf("") } val viewState by remember(viewModel, query) { @@ -101,6 +103,12 @@ private fun SearchLoader( ) } else { AdaptiveSearchScaffold( + taskDetailScreen = { taskId, onUpPress -> + taskDetailScreen.Content( + taskId = taskId, + onUpPress = onUpPress, + ) + }, viewState = viewState, query = query, setQuery = setQuery, @@ -116,6 +124,7 @@ private fun AdaptiveSearchScaffold( query: String, setQuery: (String) -> Unit, modifier: Modifier = Modifier, + taskDetailScreen: @Composable (Long, () -> Unit) -> Unit, ) { val navigator: ThreePaneScaffoldNavigator = rememberListDetailPaneScaffoldNavigator() @@ -143,13 +152,11 @@ private fun AdaptiveSearchScaffold( AnimatedPane { val taskId = navigator.currentDestination?.contentKey if (taskId != null) { -// TODO connect with the composable -// TaskDetailScreen( -// taskId = taskId, -// onUpPress = { -// coroutineScope.launch { navigator.navigateBack() } -// }, -// ) + taskDetailScreen(taskId) { + coroutineScope.launch { + navigator.navigateBack() + } + } } else { DefaultIconTextContent( icon = Icons.Outlined.CheckCircle, diff --git a/features/task-api/.gitignore b/features/task-api/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/features/task-api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/task-api/build.gradle.kts b/features/task-api/build.gradle.kts new file mode 100644 index 000000000..8c4ce3d89 --- /dev/null +++ b/features/task-api/build.gradle.kts @@ -0,0 +1,22 @@ +import extension.setFrameworkBaseName + +plugins { + id("com.escodro.multiplatform") + id("com.escodro.kotlin-parcelable") + alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) +} + +kotlin { + setFrameworkBaseName("taskapi") + + sourceSets { + commonMain.dependencies { + implementation(compose.runtime) + } + } +} + +android { + namespace = "com.escodro.taskapi" +} diff --git a/features/task-api/src/commonMain/kotlin/com/escodro/taskapi/TaskDetailScreen.kt b/features/task-api/src/commonMain/kotlin/com/escodro/taskapi/TaskDetailScreen.kt new file mode 100644 index 000000000..7e6a86603 --- /dev/null +++ b/features/task-api/src/commonMain/kotlin/com/escodro/taskapi/TaskDetailScreen.kt @@ -0,0 +1,9 @@ +package com.escodro.taskapi + +import androidx.compose.runtime.Composable + +interface TaskDetailScreen { + + @Composable + fun Content(taskId: Long, onUpPress: () -> Unit) +} diff --git a/features/task/build.gradle.kts b/features/task/build.gradle.kts index 582205c19..d4d07b773 100644 --- a/features/task/build.gradle.kts +++ b/features/task/build.gradle.kts @@ -14,6 +14,7 @@ kotlin { commonMain.dependencies { api(projects.features.categoryApi) implementation(projects.features.alarmApi) + implementation(projects.features.taskApi) implementation(projects.domain) implementation(projects.resources) implementation(projects.libraries.di) diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/di/TaskModule.kt b/features/task/src/commonMain/kotlin/com/escodro/task/di/TaskModule.kt index 1d80830db..45862e061 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/di/TaskModule.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/di/TaskModule.kt @@ -11,8 +11,10 @@ import com.escodro.task.presentation.add.AddTaskViewModel import com.escodro.task.presentation.detail.alarm.TaskAlarmViewModel import com.escodro.task.presentation.detail.alarm.interactor.OpenAlarmScheduler import com.escodro.task.presentation.detail.alarm.interactor.OpenAlarmSchedulerImpl +import com.escodro.task.presentation.detail.main.TaskDetailScreenImpl import com.escodro.task.presentation.detail.main.TaskDetailViewModel import com.escodro.task.presentation.list.TaskListViewModel +import com.escodro.taskapi.TaskDetailScreen import org.koin.core.module.Module import org.koin.core.module.dsl.factoryOf import org.koin.dsl.bind @@ -61,6 +63,8 @@ val taskModule = module { factoryOf(::OpenAlarmSchedulerImpl) bind OpenAlarmScheduler::class + factoryOf(::TaskDetailScreenImpl) bind TaskDetailScreen::class + // Mappers factoryOf(::AlarmIntervalMapper) factoryOf(::TaskMapper) diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt new file mode 100644 index 000000000..de083a55e --- /dev/null +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt @@ -0,0 +1,15 @@ +package com.escodro.task.presentation.detail.main + +import androidx.compose.runtime.Composable +import com.escodro.taskapi.TaskDetailScreen + +internal class TaskDetailScreenImpl : TaskDetailScreen { + + @Composable + override fun Content(taskId: Long, onUpPress: () -> Unit) { + TaskDetailScreen( + taskId = taskId, + onUpPress = onUpPress, + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 748a0f203..c6f7ad33c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,7 @@ include(":app") include(":features:alarm-api") include(":features:alarm") include(":features:task") +include(":features:task-api") include(":features:category-api") include(":features:category") include(":features:preference") From 5e60c35df603d883798b5becb453c341adc24fa3 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sun, 16 Mar 2025 14:54:07 -0400 Subject: [PATCH 33/39] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20unused?= =?UTF-8?q?=20navigation=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deleted redundant navigation events and the `SearchEvent` object. --- .../com/escodro/navigationapi/event/Event.kt | 17 ----------------- .../escodro/navigationapi/event/SearchEvent.kt | 11 ----------- 2 files changed, 28 deletions(-) delete mode 100644 features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/event/SearchEvent.kt diff --git a/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/event/Event.kt b/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/event/Event.kt index 5b9f04392..b69154447 100644 --- a/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/event/Event.kt +++ b/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/event/Event.kt @@ -1,7 +1,6 @@ package com.escodro.navigationapi.event import com.escodro.navigationapi.destination.Destination -import com.escodro.navigationapi.destination.HomeDestination sealed interface Event { @@ -10,20 +9,4 @@ sealed interface Event { data object OnBack : Event { override fun nextDestination(): Destination = Destination.Back } - - data object OnTaskListClick : Event { - override fun nextDestination(): Destination = HomeDestination.TaskList - } - - data object OnSearchClick : Event { - override fun nextDestination(): Destination = HomeDestination.Search - } - - data object OnCategoryListClick : Event { - override fun nextDestination(): Destination = HomeDestination.CategoryList - } - - data object OnPreferencesClick : Event { - override fun nextDestination(): Destination = HomeDestination.Preferences - } } diff --git a/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/event/SearchEvent.kt b/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/event/SearchEvent.kt deleted file mode 100644 index 78c86cc8e..000000000 --- a/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/event/SearchEvent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.escodro.navigationapi.event - -import com.escodro.navigationapi.destination.Destination -import com.escodro.navigationapi.destination.TasksDestination - -object SearchEvent { - - data class OnTaskItemClick(val taskId: Long) : Event { - override fun nextDestination(): Destination = TasksDestination.TaskDetail(taskId = taskId) - } -} From cdf7e9fa4bcf1081797cdf154476cccab12deb13 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sun, 16 Mar 2025 15:07:39 -0400 Subject: [PATCH 34/39] =?UTF-8?q?=F0=9F=8C=87=20Add=20adaptive=20navigatio?= =?UTF-8?q?n=20for=20Preferences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a responsive UI for Preference navigation using Material 3 adaptive layout. Compact views display single-pane content, while larger screens leverage a list-detail adaptive scaffold for enhanced usability. --- .../navigation/PreferenceNavGraph.kt | 4 + .../preference/presentation/Preference.kt | 117 ++++++++++++++++-- 2 files changed, 113 insertions(+), 8 deletions(-) diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt index e2d871259..691ba6b06 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt @@ -1,5 +1,6 @@ package com.escodro.preference.navigation +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.escodro.designsystem.animation.SlideInHorizontallyTransition @@ -9,6 +10,7 @@ import com.escodro.navigationapi.destination.HomeDestination import com.escodro.navigationapi.destination.PreferenceDestination import com.escodro.navigationapi.event.Event import com.escodro.navigationapi.event.PreferenceEvent +import com.escodro.navigationapi.extension.isCompact import com.escodro.navigationapi.provider.NavGraph import com.escodro.preference.presentation.AboutScreen import com.escodro.preference.presentation.OpenSource @@ -20,7 +22,9 @@ internal class PreferenceNavGraph( ) : NavGraph { override val navGraph: NavGraphBuilder.(NavEventController) -> Unit = { navEventController -> composable { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass PreferenceSection( + isCompact = windowSizeClass.isCompact(), onAboutClick = { navEventController.sendEvent(PreferenceEvent.OnAboutClick) }, onOpenSourceClick = { navEventController.sendEvent(PreferenceEvent.OnLicensesClick) }, onTrackerClick = { navEventController.sendEvent(PreferenceEvent.OnTrackerClick) }, diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt index 005da7e1e..752b2b8eb 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt @@ -8,16 +8,24 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.escodro.preference.model.AppThemeOptions +import com.escodro.preference.provider.TrackerProvider import com.escodro.resources.Res import com.escodro.resources.preference_title_features import com.escodro.resources.preference_title_settings +import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject @@ -28,12 +36,14 @@ import org.koin.compose.koinInject */ @Composable fun PreferenceSection( + isCompact: Boolean, onAboutClick: () -> Unit, onOpenSourceClick: () -> Unit, onTrackerClick: () -> Unit, modifier: Modifier = Modifier, ) { PreferenceLoader( + isCompact = isCompact, modifier = modifier, onAboutClick = onAboutClick, onTrackerClick = onTrackerClick, @@ -43,24 +53,35 @@ fun PreferenceSection( @Composable private fun PreferenceLoader( + isCompact: Boolean, onAboutClick: () -> Unit, onTrackerClick: () -> Unit, onOpenSourceClick: () -> Unit, modifier: Modifier = Modifier, viewModel: PreferenceViewModel = koinInject(), + trackerProvider: TrackerProvider = koinInject(), ) { val theme by remember(viewModel) { viewModel.loadCurrentTheme() }.collectAsState(initial = AppThemeOptions.SYSTEM) - PreferenceContent( - onAboutClick = onAboutClick, - onTrackerClick = onTrackerClick, - onOpenSourceClick = onOpenSourceClick, - theme = theme, - onThemeUpdate = viewModel::updateTheme, - modifier = modifier, - ) + if (isCompact) { + PreferenceContent( + onAboutClick = onAboutClick, + onTrackerClick = onTrackerClick, + onOpenSourceClick = onOpenSourceClick, + theme = theme, + onThemeUpdate = viewModel::updateTheme, + modifier = modifier, + ) + } else { + AdaptivePreferenceScaffold( + theme = theme, + onThemeUpdate = viewModel::updateTheme, + trackerProvider = trackerProvider, + modifier = modifier, + ) + } } @Suppress("LongParameterList") @@ -85,6 +106,77 @@ internal fun PreferenceContent( } } +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +private fun AdaptivePreferenceScaffold( + theme: AppThemeOptions, + onThemeUpdate: (AppThemeOptions) -> Unit, + trackerProvider: TrackerProvider, + modifier: Modifier = Modifier, +) { + val navigator: ThreePaneScaffoldNavigator = + rememberListDetailPaneScaffoldNavigator() + val coroutineScope = rememberCoroutineScope() + + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + PreferenceContent( + onTrackerClick = { + coroutineScope.launch { + navigator.navigateTo( + ListDetailPaneScaffoldRole.Detail, + PreferenceItem.TRACKER, + ) + } + }, + onAboutClick = { + coroutineScope.launch { + navigator.navigateTo( + ListDetailPaneScaffoldRole.Detail, + PreferenceItem.ABOUT, + ) + } + }, + onOpenSourceClick = { + coroutineScope.launch { + navigator.navigateTo( + ListDetailPaneScaffoldRole.Detail, + PreferenceItem.OPEN_SOURCE, + ) + } + }, + theme = theme, + onThemeUpdate = onThemeUpdate, + modifier = modifier, + ) + }, + detailPane = { + val detail = navigator.currentDestination?.contentKey + when (detail) { + PreferenceItem.TRACKER -> { + trackerProvider.Content( + onUpPress = { coroutineScope.launch { navigator.navigateBack() } }, + ) + } + + PreferenceItem.ABOUT -> { + AboutScreen(onUpPress = { coroutineScope.launch { navigator.navigateBack() } }) + } + + PreferenceItem.OPEN_SOURCE -> { + OpenSource(onUpPress = { coroutineScope.launch { navigator.navigateBack() } }) + } + + else -> { + // Render nothing + } + } + }, + ) +} + @Composable private fun Separator() { Spacer( @@ -95,3 +187,12 @@ private fun Separator() { .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.7F)), ) } + +/** + * Enum used to represent the preference item with the adaptive navigation. + */ +private enum class PreferenceItem { + TRACKER, + ABOUT, + OPEN_SOURCE, +} From bf501af31e5e616551752e3a32585357c03e4c68 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Sun, 16 Mar 2025 15:25:00 -0400 Subject: [PATCH 35/39] =?UTF-8?q?=F0=9F=8E=B4=20Add=20support=20for=20comp?= =?UTF-8?q?act=20mode=20in=20AlkaaToolbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced a new `isCompact` flag to the `AlkaaToolbar` component to dynamically determine the navigation icon style (back or close). Updated various screens and navigation graphs to pass the appropriate `isCompact` value. This change improves UI adaptability across different layouts. --- .../navigation/PreferenceNavGraph.kt | 2 + .../escodro/preference/presentation/About.kt | 13 ++++- .../preference/presentation/OpenSource.kt | 13 ++++- .../preference/presentation/Preference.kt | 10 +++- .../escodro/task/navigation/TaskNavGraph.kt | 1 + .../presentation/detail/main/TaskDetail.kt | 10 +++- .../detail/main/TaskDetailScreenImpl.kt | 1 + .../task/presentation/list/TaskList.kt | 1 + .../tracker/presentation/TrackerScreen.kt | 7 ++- .../components/AlkaaComponents.kt | 27 ---------- .../designsystem/components/AlkaaTopAppBar.kt | 51 +++++++++++++++++++ .../kotlin/com/escodro/alkaa/TaskFlowTest.kt | 19 +++++-- 12 files changed, 115 insertions(+), 40 deletions(-) create mode 100644 libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaTopAppBar.kt diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt index 691ba6b06..6699882e6 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt @@ -36,6 +36,7 @@ internal class PreferenceNavGraph( exitTransition = { SlideOutHorizontallyTransition }, ) { AboutScreen( + isCompact = true, onUpPress = { navEventController.sendEvent(Event.OnBack) }, ) } @@ -45,6 +46,7 @@ internal class PreferenceNavGraph( exitTransition = { SlideOutHorizontallyTransition }, ) { OpenSource( + isCompact = true, onUpPress = { navEventController.sendEvent(Event.OnBack) }, ) } diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt index afa7afb81..862e8ed03 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt @@ -42,9 +42,18 @@ import org.jetbrains.compose.resources.stringResource * Alkaa about screen. */ @Composable -fun AboutScreen(onUpPress: () -> Unit, modifier: Modifier = Modifier) { +fun AboutScreen( + isCompact: Boolean, + onUpPress: () -> Unit, + modifier: Modifier = Modifier, +) { Scaffold( - topBar = { AlkaaToolbar(onUpPress = onUpPress) }, + topBar = { + AlkaaToolbar( + isCompact = isCompact, + onUpPress = onUpPress, + ) + }, content = { paddingValues -> AboutContent(modifier = Modifier.padding(paddingValues)) }, modifier = modifier, ) diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/OpenSource.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/OpenSource.kt index 6ef5f1249..473ba4090 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/OpenSource.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/OpenSource.kt @@ -40,9 +40,18 @@ import org.jetbrains.compose.resources.stringResource * Alkaa open source licenses screen. */ @Composable -fun OpenSource(onUpPress: () -> Unit, modifier: Modifier = Modifier) { +fun OpenSource( + isCompact: Boolean, + onUpPress: () -> Unit, + modifier: Modifier = Modifier, +) { Scaffold( - topBar = { AlkaaToolbar(onUpPress = onUpPress) }, + topBar = { + AlkaaToolbar( + isCompact = isCompact, + onUpPress = onUpPress, + ) + }, content = { paddingValues -> OpenSourceContent(modifier = Modifier.padding(paddingValues)) }, modifier = modifier, ) diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt index 752b2b8eb..3213095f8 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt @@ -162,11 +162,17 @@ private fun AdaptivePreferenceScaffold( } PreferenceItem.ABOUT -> { - AboutScreen(onUpPress = { coroutineScope.launch { navigator.navigateBack() } }) + AboutScreen( + isCompact = false, + onUpPress = { coroutineScope.launch { navigator.navigateBack() } }, + ) } PreferenceItem.OPEN_SOURCE -> { - OpenSource(onUpPress = { coroutineScope.launch { navigator.navigateBack() } }) + OpenSource( + isCompact = false, + onUpPress = { coroutineScope.launch { navigator.navigateBack() } }, + ) } else -> { diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt b/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt index a28249982..312ae0b7b 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt @@ -43,6 +43,7 @@ internal class TaskNavGraph : NavGraph { ) { navEntry -> val route: TasksDestination.TaskDetail = navEntry.toRoute() TaskDetailScreen( + isCompact = true, taskId = route.taskId, onUpPress = { navEventController.sendEvent(Event.OnBack) }, ) diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt index 8528ffe6b..f254833f7 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt @@ -53,6 +53,7 @@ import kotlin.jvm.JvmInline @Suppress("LongParameterList") @Composable internal fun TaskDetailScreen( + isCompact: Boolean, taskId: Long, onUpPress: () -> Unit, detailViewModel: TaskDetailViewModel = koinInject(), @@ -84,6 +85,7 @@ internal fun TaskDetailScreen( ) TaskDetailRouter( + isCompact = isCompact, detailViewState = detailViewState, categoryViewState = categoryViewState, actions = taskDetailActions, @@ -92,11 +94,17 @@ internal fun TaskDetailScreen( @Composable internal fun TaskDetailRouter( + isCompact: Boolean, detailViewState: TaskDetailState, categoryViewState: CategoryState, actions: TaskDetailActions, ) { - Scaffold(topBar = { AlkaaToolbar(onUpPress = actions.onUpPress) }) { paddingValues -> + Scaffold(topBar = { + AlkaaToolbar( + isCompact = isCompact, + onUpPress = actions.onUpPress, + ) + }) { paddingValues -> Crossfade( targetState = detailViewState, modifier = Modifier.padding(paddingValues), diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt index de083a55e..36962b0aa 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt @@ -8,6 +8,7 @@ internal class TaskDetailScreenImpl : TaskDetailScreen { @Composable override fun Content(taskId: Long, onUpPress: () -> Unit) { TaskDetailScreen( + isCompact = false, taskId = taskId, onUpPress = onUpPress, ) diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt index ab674e555..7f3ad8189 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt @@ -168,6 +168,7 @@ private fun AdaptiveTaskListScaffold( val taskId = navigator.currentDestination?.contentKey?.value if (taskId != null) { TaskDetailScreen( + isCompact = false, taskId = taskId, onUpPress = { coroutineScope.launch { navigator.navigateBack() } diff --git a/features/tracker/src/commonMain/kotlin/com/escodro/tracker/presentation/TrackerScreen.kt b/features/tracker/src/commonMain/kotlin/com/escodro/tracker/presentation/TrackerScreen.kt index 9c4f16b8e..203db3f6a 100644 --- a/features/tracker/src/commonMain/kotlin/com/escodro/tracker/presentation/TrackerScreen.kt +++ b/features/tracker/src/commonMain/kotlin/com/escodro/tracker/presentation/TrackerScreen.kt @@ -56,7 +56,12 @@ internal fun TrackerLoader(viewModel: TrackerViewModel = koinInject(), onUpPress viewModel.loadTracker() }.collectAsState(initial = TrackerViewState.Loading) - Scaffold(topBar = { AlkaaToolbar(onUpPress = onUpPress) }) { paddingValues -> + Scaffold(topBar = { + AlkaaToolbar( + isCompact = true, + onUpPress = onUpPress, + ) + }) { paddingValues -> Crossfade(targetState = data, modifier = Modifier.padding(paddingValues)) { state -> when (state) { TrackerViewState.Empty -> TrackerEmpty() diff --git a/libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaComponents.kt b/libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaComponents.kt index e514fc8d3..f4d55e1fb 100644 --- a/libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaComponents.kt +++ b/libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaComponents.kt @@ -10,16 +10,12 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.outlined.Add -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -76,29 +72,6 @@ fun AlkaaLoadingContent(modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxSize(), content = {}) } -/** - * TopAppBar for screens that need a back button. - * - * @param onUpPress function to be called when the back/up button is clicked - * @param modifier Compose modifier - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AlkaaToolbar(onUpPress: () -> Unit, modifier: Modifier = Modifier) { - TopAppBar( - title = {}, - navigationIcon = { - IconButton(onClick = onUpPress) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = "Back", // TODO use localized string - ) - } - }, - modifier = modifier, - ) -} - /** * Floating Action button do add new elements. * diff --git a/libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaTopAppBar.kt b/libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaTopAppBar.kt new file mode 100644 index 000000000..5cd39c2db --- /dev/null +++ b/libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaTopAppBar.kt @@ -0,0 +1,51 @@ +package com.escodro.designsystem.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector + +/** + * TopAppBar for screens that need a back button. + * + * @param isCompact flag to indicate if the screen is in compact mode - if true it shows the "back" + * button, otherwise it shows the "close" button + * @param onUpPress function to be called when the back/up button is clicked + * @param modifier Compose modifier + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AlkaaToolbar( + isCompact: Boolean, + onUpPress: () -> Unit, + modifier: Modifier = Modifier, +) { + val iconType = if (isCompact) IconType.Back else IconType.Close + + TopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = onUpPress) { + Icon( + imageVector = iconType.imageVector, + contentDescription = iconType.contentDescription, + ) + } + }, + modifier = modifier, + ) +} + +enum class IconType( + val imageVector: ImageVector, + val contentDescription: String?, +) { + Back(imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back"), + Close(imageVector = Icons.Rounded.Close, contentDescription = "Close"), +} diff --git a/shared/src/commonTest/kotlin/com/escodro/alkaa/TaskFlowTest.kt b/shared/src/commonTest/kotlin/com/escodro/alkaa/TaskFlowTest.kt index 87066caa4..74fc770b9 100644 --- a/shared/src/commonTest/kotlin/com/escodro/alkaa/TaskFlowTest.kt +++ b/shared/src/commonTest/kotlin/com/escodro/alkaa/TaskFlowTest.kt @@ -94,7 +94,7 @@ internal class TaskFlowTest : KoinTest { // Edit the name of the task val newName = "Water plants" onAllNodes(hasSetTextAction())[0].performTextReplacement(newName) - onNodeWithContentDescription("Back", useUnmergedTree = true).performClick() + navigateBack() // Validate if the new name is shown onNodeWithText(text = newName, useUnmergedTree = true).assertExists() @@ -108,7 +108,7 @@ internal class TaskFlowTest : KoinTest { // Add a description val description = "Phoebe Bridgers" onAllNodes(hasSetTextAction())[1].performTextReplacement(description) - onNodeWithContentDescription("Back", useUnmergedTree = true).performClick() + navigateBack() // Reopen the task and validate if the description is save waitUntilAtLeastOneExists(hasText(taskName)) @@ -124,7 +124,7 @@ internal class TaskFlowTest : KoinTest { // Select a category val category = "Music" onAllNodes(hasText(category)).onLast().performClick() - onNodeWithContentDescription("Back", useUnmergedTree = true).performClick() + navigateBack() // Reopen the task and validate if the category is selected waitUntilAtLeastOneExists(hasText(taskName)) @@ -140,7 +140,7 @@ internal class TaskFlowTest : KoinTest { onNodeWithText("Next").performClick() onNodeWithText("Confirm").performClick() - onNodeWithContentDescription("Back", useUnmergedTree = true).performClick() + navigateBack() // Reopen the task and validate if the alarm is on waitUntilAtLeastOneExists(hasText(taskName)) @@ -162,7 +162,7 @@ internal class TaskFlowTest : KoinTest { onNodeWithText(alarmArray[0]).performClick() onNodeWithText(alarmArray.last()).performClick() - onNodeWithContentDescription("Back", useUnmergedTree = true).performClick() + navigateBack() // Reopen the task and validate if the alarm is on waitUntilAtLeastOneExists(hasText(taskName)) @@ -205,4 +205,13 @@ internal class TaskFlowTest : KoinTest { waitUntilAtLeastOneExists(hasText(taskName)) onAllNodes(hasSetTextAction()).assertAny(hasText(taskName, substring = true)) } + + /** + * Force the navigation back to the previous screen based on the content description. + */ + private fun ComposeUiTest.navigateBack() = try { + onNodeWithContentDescription("Back", substring = true).performClick() + } catch (e: AssertionError) { + onNodeWithContentDescription("Close", substring = true).performClick() + } } From 7a89bacc28917f771a3a0782f33c893edfb774c9 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Tue, 18 Mar 2025 12:50:16 -0400 Subject: [PATCH 36/39] =?UTF-8?q?=F0=9F=94=AE=20Rename=20"isCompact"=20to?= =?UTF-8?q?=20"isSinglePane"=20for=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The function was renamed from "isCompact" to "isSinglePane" to make it clear its usage. --- .../extension/WindowSizeClassExtension.kt | 7 ++++--- .../preference/navigation/PreferenceNavGraph.kt | 9 ++++----- .../com/escodro/preference/presentation/About.kt | 4 ++-- .../escodro/preference/presentation/OpenSource.kt | 4 ++-- .../escodro/preference/presentation/Preference.kt | 12 ++++++------ .../com/escodro/search/navigation/SearchNavGraph.kt | 5 ++--- .../kotlin/com/escodro/search/presentation/Search.kt | 8 ++++---- .../com/escodro/task/navigation/TaskNavGraph.kt | 7 +++---- .../task/presentation/detail/main/TaskDetail.kt | 8 ++++---- .../presentation/detail/main/TaskDetailScreenImpl.kt | 2 +- .../com/escodro/task/presentation/list/TaskList.kt | 10 +++++----- .../escodro/tracker/presentation/TrackerScreen.kt | 2 +- .../kotlin/com/escodro/appstate/AlkaaAppState.kt | 2 -- .../designsystem/components/AlkaaTopAppBar.kt | 6 +++--- 14 files changed, 41 insertions(+), 45 deletions(-) diff --git a/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt b/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt index 4b654fbf8..1c579528d 100644 --- a/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt +++ b/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt @@ -5,10 +5,11 @@ import androidx.window.core.layout.WindowSizeClass import androidx.window.core.layout.WindowWidthSizeClass /** - * Check if the [WindowSizeClass] is compact. + * Check if the [WindowSizeClass] has a compact width, meaning that the layout should be displayed + * in a single pane. * - * @return true if the [WindowSizeClass] is compact, false otherwise + * @return true if the [WindowSizeClass] is single pane, false otherwise. */ -fun WindowSizeClass.isCompact(): Boolean = +fun WindowSizeClass.isSinglePane(): Boolean = windowWidthSizeClass == WindowWidthSizeClass.COMPACT || windowHeightSizeClass == WindowHeightSizeClass.COMPACT diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt index 6699882e6..9a0c98f19 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/navigation/PreferenceNavGraph.kt @@ -10,7 +10,7 @@ import com.escodro.navigationapi.destination.HomeDestination import com.escodro.navigationapi.destination.PreferenceDestination import com.escodro.navigationapi.event.Event import com.escodro.navigationapi.event.PreferenceEvent -import com.escodro.navigationapi.extension.isCompact +import com.escodro.navigationapi.extension.isSinglePane import com.escodro.navigationapi.provider.NavGraph import com.escodro.preference.presentation.AboutScreen import com.escodro.preference.presentation.OpenSource @@ -22,9 +22,8 @@ internal class PreferenceNavGraph( ) : NavGraph { override val navGraph: NavGraphBuilder.(NavEventController) -> Unit = { navEventController -> composable { - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass PreferenceSection( - isCompact = windowSizeClass.isCompact(), + isSinglePane = currentWindowAdaptiveInfo().windowSizeClass.isSinglePane(), onAboutClick = { navEventController.sendEvent(PreferenceEvent.OnAboutClick) }, onOpenSourceClick = { navEventController.sendEvent(PreferenceEvent.OnLicensesClick) }, onTrackerClick = { navEventController.sendEvent(PreferenceEvent.OnTrackerClick) }, @@ -36,7 +35,7 @@ internal class PreferenceNavGraph( exitTransition = { SlideOutHorizontallyTransition }, ) { AboutScreen( - isCompact = true, + isSinglePane = true, onUpPress = { navEventController.sendEvent(Event.OnBack) }, ) } @@ -46,7 +45,7 @@ internal class PreferenceNavGraph( exitTransition = { SlideOutHorizontallyTransition }, ) { OpenSource( - isCompact = true, + isSinglePane = true, onUpPress = { navEventController.sendEvent(Event.OnBack) }, ) } diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt index 862e8ed03..46d48b61a 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/About.kt @@ -43,14 +43,14 @@ import org.jetbrains.compose.resources.stringResource */ @Composable fun AboutScreen( - isCompact: Boolean, + isSinglePane: Boolean, onUpPress: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( topBar = { AlkaaToolbar( - isCompact = isCompact, + isSinglePane = isSinglePane, onUpPress = onUpPress, ) }, diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/OpenSource.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/OpenSource.kt index 473ba4090..fdfc76f71 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/OpenSource.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/OpenSource.kt @@ -41,14 +41,14 @@ import org.jetbrains.compose.resources.stringResource */ @Composable fun OpenSource( - isCompact: Boolean, + isSinglePane: Boolean, onUpPress: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( topBar = { AlkaaToolbar( - isCompact = isCompact, + isSinglePane = isSinglePane, onUpPress = onUpPress, ) }, diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt index 3213095f8..14c6a033c 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt @@ -36,14 +36,14 @@ import org.koin.compose.koinInject */ @Composable fun PreferenceSection( - isCompact: Boolean, + isSinglePane: Boolean, onAboutClick: () -> Unit, onOpenSourceClick: () -> Unit, onTrackerClick: () -> Unit, modifier: Modifier = Modifier, ) { PreferenceLoader( - isCompact = isCompact, + isSinglePane = isSinglePane, modifier = modifier, onAboutClick = onAboutClick, onTrackerClick = onTrackerClick, @@ -53,7 +53,7 @@ fun PreferenceSection( @Composable private fun PreferenceLoader( - isCompact: Boolean, + isSinglePane: Boolean, onAboutClick: () -> Unit, onTrackerClick: () -> Unit, onOpenSourceClick: () -> Unit, @@ -65,7 +65,7 @@ private fun PreferenceLoader( viewModel.loadCurrentTheme() }.collectAsState(initial = AppThemeOptions.SYSTEM) - if (isCompact) { + if (isSinglePane) { PreferenceContent( onAboutClick = onAboutClick, onTrackerClick = onTrackerClick, @@ -163,14 +163,14 @@ private fun AdaptivePreferenceScaffold( PreferenceItem.ABOUT -> { AboutScreen( - isCompact = false, + isSinglePane = false, onUpPress = { coroutineScope.launch { navigator.navigateBack() } }, ) } PreferenceItem.OPEN_SOURCE -> { OpenSource( - isCompact = false, + isSinglePane = false, onUpPress = { coroutineScope.launch { navigator.navigateBack() } }, ) } diff --git a/features/search/src/commonMain/kotlin/com/escodro/search/navigation/SearchNavGraph.kt b/features/search/src/commonMain/kotlin/com/escodro/search/navigation/SearchNavGraph.kt index 5bad806bf..dd196208f 100644 --- a/features/search/src/commonMain/kotlin/com/escodro/search/navigation/SearchNavGraph.kt +++ b/features/search/src/commonMain/kotlin/com/escodro/search/navigation/SearchNavGraph.kt @@ -6,7 +6,7 @@ import androidx.navigation.compose.composable import com.escodro.navigationapi.controller.NavEventController import com.escodro.navigationapi.destination.HomeDestination import com.escodro.navigationapi.event.TaskEvent -import com.escodro.navigationapi.extension.isCompact +import com.escodro.navigationapi.extension.isSinglePane import com.escodro.navigationapi.provider.NavGraph import com.escodro.search.presentation.SearchSection @@ -14,9 +14,8 @@ internal class SearchNavGraph : NavGraph { override val navGraph: NavGraphBuilder.(NavEventController) -> Unit = { navEventController -> composable { - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass SearchSection( - isCompact = windowSizeClass.isCompact(), + isSinglePane = currentWindowAdaptiveInfo().windowSizeClass.isSinglePane(), onItemClick = { navEventController.sendEvent(TaskEvent.OnTaskClick(id = it)) }, ) } diff --git a/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt b/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt index 184b39b1e..5e6f7bef3 100644 --- a/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt +++ b/features/search/src/commonMain/kotlin/com/escodro/search/presentation/Search.kt @@ -69,12 +69,12 @@ import org.koin.compose.koinInject */ @Composable fun SearchSection( - isCompact: Boolean, + isSinglePane: Boolean, onItemClick: (Long) -> Unit, modifier: Modifier = Modifier, ) { SearchLoader( - isCompact = isCompact, + isSinglePane = isSinglePane, modifier = modifier, onItemClick = onItemClick, ) @@ -82,7 +82,7 @@ fun SearchSection( @Composable private fun SearchLoader( - isCompact: Boolean, + isSinglePane: Boolean, onItemClick: (Long) -> Unit, modifier: Modifier = Modifier, viewModel: SearchViewModel = koinInject(), @@ -93,7 +93,7 @@ private fun SearchLoader( viewModel.findTasksByName(query) }.collectAsState(initial = SearchViewState.Loading) - if (isCompact) { + if (isSinglePane) { SearchScaffold( viewState = viewState, modifier = modifier, diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt b/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt index 312ae0b7b..883f3ea5b 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/navigation/TaskNavGraph.kt @@ -14,7 +14,7 @@ import com.escodro.navigationapi.destination.HomeDestination import com.escodro.navigationapi.destination.TasksDestination import com.escodro.navigationapi.event.Event import com.escodro.navigationapi.event.TaskEvent -import com.escodro.navigationapi.extension.isCompact +import com.escodro.navigationapi.extension.isSinglePane import com.escodro.navigationapi.provider.NavGraph import com.escodro.task.presentation.add.AddTaskBottomSheet import com.escodro.task.presentation.detail.main.TaskDetailScreen @@ -24,9 +24,8 @@ internal class TaskNavGraph : NavGraph { override val navGraph: NavGraphBuilder.(NavEventController) -> Unit = { navEventController -> composable { - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass TaskListSection( - isCompact = windowSizeClass.isCompact(), + isSinglePane = currentWindowAdaptiveInfo().windowSizeClass.isSinglePane(), onItemClick = { taskId -> navEventController.sendEvent(TaskEvent.OnTaskClick(taskId)) }, onFabClick = { navEventController.sendEvent(TaskEvent.OnNewTaskClick) }, ) @@ -43,7 +42,7 @@ internal class TaskNavGraph : NavGraph { ) { navEntry -> val route: TasksDestination.TaskDetail = navEntry.toRoute() TaskDetailScreen( - isCompact = true, + isSinglePane = true, taskId = route.taskId, onUpPress = { navEventController.sendEvent(Event.OnBack) }, ) diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt index f254833f7..f29790437 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetail.kt @@ -53,7 +53,7 @@ import kotlin.jvm.JvmInline @Suppress("LongParameterList") @Composable internal fun TaskDetailScreen( - isCompact: Boolean, + isSinglePane: Boolean, taskId: Long, onUpPress: () -> Unit, detailViewModel: TaskDetailViewModel = koinInject(), @@ -85,7 +85,7 @@ internal fun TaskDetailScreen( ) TaskDetailRouter( - isCompact = isCompact, + isSinglePane = isSinglePane, detailViewState = detailViewState, categoryViewState = categoryViewState, actions = taskDetailActions, @@ -94,14 +94,14 @@ internal fun TaskDetailScreen( @Composable internal fun TaskDetailRouter( - isCompact: Boolean, + isSinglePane: Boolean, detailViewState: TaskDetailState, categoryViewState: CategoryState, actions: TaskDetailActions, ) { Scaffold(topBar = { AlkaaToolbar( - isCompact = isCompact, + isSinglePane = isSinglePane, onUpPress = actions.onUpPress, ) }) { paddingValues -> diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt index 36962b0aa..15055047c 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/detail/main/TaskDetailScreenImpl.kt @@ -8,7 +8,7 @@ internal class TaskDetailScreenImpl : TaskDetailScreen { @Composable override fun Content(taskId: Long, onUpPress: () -> Unit) { TaskDetailScreen( - isCompact = false, + isSinglePane = false, taskId = taskId, onUpPress = onUpPress, ) diff --git a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt index 7f3ad8189..2bef59b1d 100644 --- a/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt +++ b/features/task/src/commonMain/kotlin/com/escodro/task/presentation/list/TaskList.kt @@ -67,13 +67,13 @@ import org.koin.compose.koinInject */ @Composable fun TaskListSection( - isCompact: Boolean, + isSinglePane: Boolean, onItemClick: (Long) -> Unit, onFabClick: () -> Unit, modifier: Modifier = Modifier, ) { TaskListLoader( - isCompact = isCompact, + isSinglePane = isSinglePane, modifier = modifier, onFabClick = onFabClick, onItemClick = onItemClick, @@ -82,7 +82,7 @@ fun TaskListSection( @Composable internal fun TaskListLoader( - isCompact: Boolean, + isSinglePane: Boolean, onItemClick: (Long) -> Unit, onFabClick: () -> Unit, modifier: Modifier = Modifier, @@ -100,7 +100,7 @@ internal fun TaskListLoader( categoryViewModel.loadCategories() }.collectAsState(initial = CategoryState.Loading) - if (isCompact) { + if (isSinglePane) { TaskListScaffold( taskViewState = taskViewState, categoryViewState = categoryViewState, @@ -168,7 +168,7 @@ private fun AdaptiveTaskListScaffold( val taskId = navigator.currentDestination?.contentKey?.value if (taskId != null) { TaskDetailScreen( - isCompact = false, + isSinglePane = false, taskId = taskId, onUpPress = { coroutineScope.launch { navigator.navigateBack() } diff --git a/features/tracker/src/commonMain/kotlin/com/escodro/tracker/presentation/TrackerScreen.kt b/features/tracker/src/commonMain/kotlin/com/escodro/tracker/presentation/TrackerScreen.kt index 203db3f6a..acb8f7114 100644 --- a/features/tracker/src/commonMain/kotlin/com/escodro/tracker/presentation/TrackerScreen.kt +++ b/features/tracker/src/commonMain/kotlin/com/escodro/tracker/presentation/TrackerScreen.kt @@ -58,7 +58,7 @@ internal fun TrackerLoader(viewModel: TrackerViewModel = koinInject(), onUpPress Scaffold(topBar = { AlkaaToolbar( - isCompact = true, + isSinglePane = true, onUpPress = onUpPress, ) }) { paddingValues -> diff --git a/libraries/appstate/src/commonMain/kotlin/com/escodro/appstate/AlkaaAppState.kt b/libraries/appstate/src/commonMain/kotlin/com/escodro/appstate/AlkaaAppState.kt index e4e9eb73e..937fb3f0d 100644 --- a/libraries/appstate/src/commonMain/kotlin/com/escodro/appstate/AlkaaAppState.kt +++ b/libraries/appstate/src/commonMain/kotlin/com/escodro/appstate/AlkaaAppState.kt @@ -15,8 +15,6 @@ import kotlinx.coroutines.flow.mapLatest /** * Alkaa App state. - * - * @property isCompactMode flag to indicate if the app is in compact mode */ @Stable data class AlkaaAppState( diff --git a/libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaTopAppBar.kt b/libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaTopAppBar.kt index 5cd39c2db..db2170761 100644 --- a/libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaTopAppBar.kt +++ b/libraries/designsystem/src/commonMain/kotlin/com/escodro/designsystem/components/AlkaaTopAppBar.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.graphics.vector.ImageVector /** * TopAppBar for screens that need a back button. * - * @param isCompact flag to indicate if the screen is in compact mode - if true it shows the "back" + * @param isSinglePane flag to indicate if the screen is in compact mode - if true it shows the "back" * button, otherwise it shows the "close" button * @param onUpPress function to be called when the back/up button is clicked * @param modifier Compose modifier @@ -22,11 +22,11 @@ import androidx.compose.ui.graphics.vector.ImageVector @OptIn(ExperimentalMaterial3Api::class) @Composable fun AlkaaToolbar( - isCompact: Boolean, + isSinglePane: Boolean, onUpPress: () -> Unit, modifier: Modifier = Modifier, ) { - val iconType = if (isCompact) IconType.Back else IconType.Close + val iconType = if (isSinglePane) IconType.Back else IconType.Close TopAppBar( title = {}, From b5e4b14b6c22782c668a8a046b8cfa67ea354833 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Tue, 18 Mar 2025 12:51:40 -0400 Subject: [PATCH 37/39] =?UTF-8?q?=F0=9F=A7=BA=20Update=20the=20"isSinglePa?= =?UTF-8?q?ne()"=20function=20to=20only=20consider=20width=20size?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To better match the Google recommendations and the adaptive layouts, only the Width Size Class is now considered. This change will allow the multi-pane layout on regular devices in landscape. --- .../navigationapi/extension/WindowSizeClassExtension.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt b/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt index 1c579528d..5c4d1fefe 100644 --- a/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt +++ b/features/navigation-api/src/commonMain/kotlin/com/escodro/navigationapi/extension/WindowSizeClassExtension.kt @@ -1,6 +1,5 @@ package com.escodro.navigationapi.extension -import androidx.window.core.layout.WindowHeightSizeClass import androidx.window.core.layout.WindowSizeClass import androidx.window.core.layout.WindowWidthSizeClass @@ -11,5 +10,4 @@ import androidx.window.core.layout.WindowWidthSizeClass * @return true if the [WindowSizeClass] is single pane, false otherwise. */ fun WindowSizeClass.isSinglePane(): Boolean = - windowWidthSizeClass == WindowWidthSizeClass.COMPACT || - windowHeightSizeClass == WindowHeightSizeClass.COMPACT + windowWidthSizeClass == WindowWidthSizeClass.COMPACT From 24b340eb90e88a8d9d69163a512d0fa261924995 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Tue, 18 Mar 2025 12:56:01 -0400 Subject: [PATCH 38/39] =?UTF-8?q?=E2=9C=A8=20Add=20vertical=20scrolling=20?= =?UTF-8?q?to=20preferences=20column=20for=20better=20usability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Missing scrolling added. --- .../com/escodro/preference/presentation/Preference.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt index 14c6a033c..da83badbd 100644 --- a/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt +++ b/features/preference/src/commonMain/kotlin/com/escodro/preference/presentation/Preference.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold @@ -94,7 +96,11 @@ internal fun PreferenceContent( onThemeUpdate: (AppThemeOptions) -> Unit, modifier: Modifier = Modifier, ) { - Column(modifier = modifier.fillMaxSize()) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { PreferenceTitle(title = stringResource(Res.string.preference_title_features)) TrackerItem(onTrackerClick) Separator() From 5290333624137083c455759ee324602b4579bbd4 Mon Sep 17 00:00:00 2001 From: Igor Escodro Date: Tue, 18 Mar 2025 13:12:41 -0400 Subject: [PATCH 39/39] =?UTF-8?q?=F0=9F=94=96=20Release=20Alkaa=203.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New version released --- gradle/libs.versions.toml | 4 ++-- ios-app/alkaa.xcodeproj/project.pbxproj | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dcb708d5e..494734300 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # Project -version_code = "30200" -version_name = "3.2.0" +version_code = "30300" +version_name = "3.3.0" android_gradle_plugin = "8.9.0" kotlin = "2.1.10" android_sdk_compile = "35" diff --git a/ios-app/alkaa.xcodeproj/project.pbxproj b/ios-app/alkaa.xcodeproj/project.pbxproj index 2a62d354f..86c044f0e 100644 --- a/ios-app/alkaa.xcodeproj/project.pbxproj +++ b/ios-app/alkaa.xcodeproj/project.pbxproj @@ -316,7 +316,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 30200; + CURRENT_PROJECT_VERSION = 30300; DEVELOPMENT_ASSET_PATHS = "\"alkaa/Preview Content\""; DEVELOPMENT_TEAM = 5Z3927U4ZC; ENABLE_PREVIEWS = YES; @@ -335,7 +335,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.2.0; + MARKETING_VERSION = 3.3.0; OTHER_LDFLAGS = ( "$(inherited)", "-framework", @@ -358,7 +358,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 30200; + CURRENT_PROJECT_VERSION = 30300; DEVELOPMENT_ASSET_PATHS = "\"alkaa/Preview Content\""; DEVELOPMENT_TEAM = 5Z3927U4ZC; ENABLE_PREVIEWS = YES; @@ -377,7 +377,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.2.0; + MARKETING_VERSION = 3.3.0; OTHER_LDFLAGS = ( "$(inherited)", "-framework",