From 3b47fa025b7593269c91c1e3f489c4b49e15773c Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Tue, 15 Aug 2023 17:18:28 +0300 Subject: [PATCH 01/20] feat: watchos - added watchos module - added simple watchos screens - added watchos tile --- .gitignore | 1 + androidApp/build.gradle.kts | 1 + .../features/status/StubStatusComponent.kt | 48 +++++ .../features/theme/PreviewThemeSwitcher.kt | 25 +++ .../mobile/features/theme/ThemeSwitcher.kt | 1 + .../features/theme/ThemeSwitcherComponent.kt | 5 + .../features/ui/root/ComposeApplication.kt | 15 +- settings.gradle.kts | 1 + wearApp/build.gradle.kts | 126 ++++++++++++ wearApp/src/main/AndroidManifest.xml | 89 ++++++++ .../empireprojekt/mobile/wear/App.kt | 16 ++ .../empireprojekt/mobile/wear/MainActivity.kt | 24 +++ .../complication/MainComplicationService.kt | 41 ++++ .../mobile/wear/di/WearRootModule.kt | 16 ++ .../mobile/wear/di/impl/WearRootModuleImpl.kt | 21 ++ .../wear/features/components/AstraChip.kt | 34 ++++ .../mobile/wear/features/main/MainScreen.kt | 46 +++++ .../wear/features/main/components/NavChip.kt | 41 ++++ .../features/main/components/ThemeChip.kt | 60 ++++++ .../main/preview/RootScreenPreview.kt | 15 ++ .../wear/features/status/StatusesScreen.kt | 43 ++++ .../status/components/StatusWidget.kt | 61 ++++++ .../status/preview/StatusesScreenPreview.kt | 18 ++ .../mobile/wear/tile/ComposeTileService.kt | 132 ++++++++++++ .../mobile/wear/tile/MainTileService.kt | 190 ++++++++++++++++++ 25 files changed, 1060 insertions(+), 10 deletions(-) create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StubStatusComponent.kt create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcher.kt create mode 100644 wearApp/build.gradle.kts create mode 100644 wearApp/src/main/AndroidManifest.xml create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/App.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/complication/MainComplicationService.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/components/AstraChip.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/NavChip.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/components/StatusWidget.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/preview/StatusesScreenPreview.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/ComposeTileService.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt diff --git a/.gitignore b/.gitignore index a3a6e2ef..44b5b08d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ androidApp/private_key.pepk androidApp/google-services.json androidApp/keystore.jks desktop/build +wearApp/build keys.properties # iosApp iosApp/Pods diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 5bedb857..3b40e3ab 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -118,6 +118,7 @@ dependencies { implementation(libs.decompose.core) implementation(libs.decompose.compose.jetpack) implementation(libs.decompose.android) + implementation("com.google.android.gms:play-services-wearable:18.0.0") // Local implementation(projects.modules.features.root) implementation(projects.modules.features.ui) diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StubStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StubStatusComponent.kt new file mode 100644 index 00000000..d3d7d50c --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StubStatusComponent.kt @@ -0,0 +1,48 @@ +package com.makeevrserg.empireprojekt.mobile.features.status + +import com.makeevrserg.empireprojekt.mobile.services.core.AnyStateFlow +import com.makeevrserg.empireprojekt.mobile.services.core.wrapToAny +import dev.icerock.moko.resources.desc.Raw +import dev.icerock.moko.resources.desc.StringDesc +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +class StubStatusComponent : StatusComponent, CoroutineScope by MainScope() { + private val mutableStateFlow = MutableStateFlow(StubModel()) + override val model: AnyStateFlow = mutableStateFlow.wrapToAny() + + init { + launch { + while (isActive) { + delay(1000L) + checkStatus() + } + } + } + + override fun checkStatus() { + launch { + mutableStateFlow.update { + it.copy(isLoading = true) + } + delay(500L) + mutableStateFlow.update { + it.copy( + isLoading = false, + status = StatusComponent.Model.LoadingStatus.values().random() + ) + } + } + } + + private data class StubModel( + override val title: StringDesc = StringDesc.Raw("Stub Title"), + override val isLoading: Boolean = true, + override val status: StatusComponent.Model.LoadingStatus = StatusComponent.Model.LoadingStatus.LOADING + ) : StatusComponent.Model +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcher.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcher.kt new file mode 100644 index 00000000..69ed4228 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcher.kt @@ -0,0 +1,25 @@ +package com.makeevrserg.empireprojekt.mobile.features.theme + +import kotlinx.coroutines.flow.MutableStateFlow +import ru.astrainteractive.klibs.mikro.core.util.next + +class PreviewThemeSwitcher : ThemeSwitcher { + override val theme: MutableStateFlow = + MutableStateFlow(ThemeSwitcher.Theme.LIGHT) + + override fun selectDarkTheme() { + selectTheme(ThemeSwitcher.Theme.DARK) + } + + override fun selectLightTheme() { + selectTheme(ThemeSwitcher.Theme.LIGHT) + } + + override fun selectTheme(theme: ThemeSwitcher.Theme) { + this.theme.value = theme + } + + override fun next() { + theme.value.next(ThemeSwitcher.Theme.values()).run(::selectTheme) + } +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcher.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcher.kt index fdb4f1ea..4cd49c17 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcher.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcher.kt @@ -12,4 +12,5 @@ interface ThemeSwitcher { fun selectDarkTheme() fun selectLightTheme() fun selectTheme(theme: Theme) + fun next() } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt index 6bb3ca92..9fa9e3d1 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt @@ -3,6 +3,7 @@ package com.makeevrserg.empireprojekt.mobile.features.theme import com.russhwolf.settings.Settings import kotlinx.coroutines.flow.StateFlow import ru.astrainteractive.klibs.kstorage.StateFlowMutableStorageValue +import ru.astrainteractive.klibs.mikro.core.util.next class ThemeSwitcherComponent(private val settings: Settings) : ThemeSwitcher { private val key = "THEME" @@ -31,6 +32,10 @@ class ThemeSwitcherComponent(private val settings: Settings) : ThemeSwitcher { themeFlowStorageValue.save(theme) } + override fun next() { + selectTheme(theme.value.next(ThemeSwitcher.Theme.values())) + } + init { themeFlowStorageValue.load() } diff --git a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt index bb963f6e..ba620871 100644 --- a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt +++ b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt @@ -1,10 +1,6 @@ package com.makeevrserg.empireprojekt.mobile.features.ui.root -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.with +import androidx.compose.animation.Crossfade import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -12,23 +8,22 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme import com.makeevrserg.empireprojekt.mobile.core.ui.theme.LocalAppTheme +import com.makeevrserg.empireprojekt.mobile.features.theme.PreviewThemeSwitcher import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher -private fun ThemeSwitcher.Theme.toComposeTheme() = when (this) { +fun ThemeSwitcher.Theme.toComposeTheme() = when (this) { ThemeSwitcher.Theme.DARK -> AppTheme.DefaultDarkTheme ThemeSwitcher.Theme.LIGHT -> AppTheme.DefaultLightTheme } -@OptIn(ExperimentalAnimationApi::class) @Composable -fun ComposeApplication(themeSwitcher: ThemeSwitcher, content: @Composable () -> Unit) { +fun ComposeApplication(themeSwitcher: ThemeSwitcher = PreviewThemeSwitcher(), content: @Composable () -> Unit) { val theme by themeSwitcher.theme.collectAsState() val appTheme = theme.toComposeTheme() TransparentBars(appTheme.isDark) - AnimatedContent( + Crossfade( targetState = appTheme, - transitionSpec = { fadeIn() with fadeOut() } ) { appTheme -> CompositionLocalProvider( LocalAppTheme provides appTheme, diff --git a/settings.gradle.kts b/settings.gradle.kts index 021ff1d5..244212a0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,6 +23,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") rootProject.name = "EmpireProjekt-Mobile" // Services include(":androidApp") +include(":wearApp") include(":modules:services:resources") include(":modules:services:core-ui") include(":modules:services:core") diff --git a/wearApp/build.gradle.kts b/wearApp/build.gradle.kts new file mode 100644 index 00000000..4caf6ea0 --- /dev/null +++ b/wearApp/build.gradle.kts @@ -0,0 +1,126 @@ +import ru.astrainteractive.gradleplugin.util.GradleProperty.Companion.gradleProperty +import ru.astrainteractive.gradleplugin.util.ProjectProperties.jinfo +import ru.astrainteractive.gradleplugin.util.ProjectProperties.projectInfo +import ru.astrainteractive.gradleplugin.util.SecretProperty.Companion.secretProperty + +plugins { + kotlin("plugin.serialization") + id("com.android.application") + id("kotlin-android") + id("ru.astrainteractive.gradleplugin.java.core") + id("ru.astrainteractive.gradleplugin.android.apk.name") +} + +android { + namespace = "${projectInfo.group}" + compileSdk = gradleProperty("android.sdk.compile").integer + + defaultConfig { + applicationId = "${projectInfo.group}" + minSdk = 26 + targetSdk = gradleProperty("android.sdk.target").integer + versionCode = gradleProperty("project.version.code").integer + versionName = projectInfo.versionString + vectorDrawables { + useSupportLibrary = true + } + } + + compileOptions { + sourceCompatibility = jinfo.jsource + targetCompatibility = jinfo.jtarget + } + kotlinOptions { + jvmTarget = jinfo.jtarget.majorVersion + } + signingConfigs { + val secretKeyAlias = runCatching { secretProperty("KEY_ALIAS").string }.getOrNull() ?: "" + val secretKeyPassword = + runCatching { secretProperty("KEY_PASSWORD").string }.getOrNull() ?: "" + val secretStorePassword = + runCatching { secretProperty("STORE_PASSWORD").string }.getOrNull() ?: "" + getByName("debug") { + if (file("../androidApp/keystore.jks").exists()) { + keyAlias = secretKeyAlias + keyPassword = secretKeyPassword + storePassword = secretStorePassword + storeFile = file("../androidApp/keystore.jks") + } + } + create("release") { + if (file("../androidApp/keystore.jks").exists()) { + keyAlias = secretKeyAlias + keyPassword = secretKeyPassword + storePassword = secretStorePassword + storeFile = file("../androidApp/keystore.jks") + } + } + } + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + signingConfig = signingConfigs.getByName("release") + } + debug { + isDebuggable = true + signingConfig = signingConfigs.getByName("debug") + } + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.kotlin.compilerExtensionVersion.get() + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + // Coroutines + implementation(libs.kotlin.coroutines.core) + implementation(libs.kotlin.coroutines.android) + // Compose + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material:material") + implementation("androidx.compose.foundation:foundation") + implementation("androidx.compose.material:material") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.compose.ui:ui-tooling") + implementation("androidx.compose.ui:ui-tooling-preview") + + implementation("androidx.glance:glance-wear-tiles:1.0.0-alpha05") + + implementation("androidx.wear.tiles:tiles:1.2.0") + implementation("androidx.wear.tiles:tiles-material:1.2.0") + implementation("com.google.android.horologist:horologist-compose-tools:0.5.3") + implementation("com.google.android.horologist:horologist-tiles:0.5.3") + implementation("androidx.wear.watchface:watchface-complications-data-source-ktx:1.1.1") + // klibs + implementation(libs.klibs.mikro.core) + implementation(libs.klibs.mikro.platform) + implementation(libs.klibs.kstorage) + implementation(libs.klibs.kdi) + // Settings + implementation(libs.mppsettings) + // Decompose + implementation(libs.decompose.core) + implementation(libs.decompose.compose.jetpack) + implementation(libs.decompose.android) + implementation("com.google.android.gms:play-services-wearable:18.0.0") + // Local + implementation(projects.modules.features.root) + implementation(projects.modules.features.ui) + implementation(projects.modules.services.coreUi) + implementation(projects.modules.services.resources) + wearApp(project(":wearApp")) +} diff --git a/wearApp/src/main/AndroidManifest.xml b/wearApp/src/main/AndroidManifest.xml new file mode 100644 index 00000000..dc17e24a --- /dev/null +++ b/wearApp/src/main/AndroidManifest.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/App.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/App.kt new file mode 100644 index 00000000..7f65917f --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/App.kt @@ -0,0 +1,16 @@ +package com.makeevrserg.empireprojekt.mobile.wear + +import android.app.Application +import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import ru.astrainteractive.klibs.mikro.platform.DefaultAndroidPlatformConfiguration + +class App : Application() { + override fun onCreate() { + super.onCreate() + WearRootModule.platformConfiguration.initialize( + DefaultAndroidPlatformConfiguration( + applicationContext + ) + ) + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt new file mode 100644 index 00000000..47bb89d5 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt @@ -0,0 +1,24 @@ +package com.makeevrserg.empireprojekt.mobile.wear + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.view.WindowCompat +import com.makeevrserg.empireprojekt.mobile.features.ui.root.ComposeApplication +import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import com.makeevrserg.empireprojekt.mobile.wear.features.main.MainScreen +import ru.astrainteractive.klibs.kdi.getValue + +class MainActivity : ComponentActivity() { + private val rootModule by WearRootModule + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + setTheme(com.makeevrserg.empireprojekt.mobile.resources.R.style.AppTheme) + setContent { + ComposeApplication(rootModule.themeSwitcher.value) { + MainScreen(wearRootModule = WearRootModule) + } + } + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/complication/MainComplicationService.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/complication/MainComplicationService.kt new file mode 100644 index 00000000..767d189e --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/complication/MainComplicationService.kt @@ -0,0 +1,41 @@ +package com.makeevrserg.empireprojekt.mobile.wear.complication + +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.ShortTextComplicationData +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService +import java.util.Calendar + +/** + * Skeleton for complication data source that returns short text. + */ +class MainComplicationService : SuspendingComplicationDataSourceService() { + + override fun getPreviewData(type: ComplicationType): ComplicationData? { + if (type != ComplicationType.SHORT_TEXT) { + return null + } + return createComplicationData("Mon", "Monday") + } + + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData { + return when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) { + Calendar.SUNDAY -> createComplicationData("Sun", "Sunday") + Calendar.MONDAY -> createComplicationData("Mon", "Monday") + Calendar.TUESDAY -> createComplicationData("Tue", "Tuesday") + Calendar.WEDNESDAY -> createComplicationData("Wed", "Wednesday") + Calendar.THURSDAY -> createComplicationData("Thu", "Thursday") + Calendar.FRIDAY -> createComplicationData("Fri!", "Friday!") + Calendar.SATURDAY -> createComplicationData("Sat", "Saturday") + else -> throw IllegalArgumentException("too many days") + } + } + + private fun createComplicationData(text: String, contentDescription: String) = + ShortTextComplicationData.Builder( + text = PlainComplicationText.Builder(text).build(), + contentDescription = PlainComplicationText.Builder(contentDescription).build() + ).build() +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt new file mode 100644 index 00000000..3e3bb462 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt @@ -0,0 +1,16 @@ +package com.makeevrserg.empireprojekt.mobile.wear.di + +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.wear.di.impl.WearRootModuleImpl +import com.russhwolf.settings.Settings +import ru.astrainteractive.klibs.kdi.Lateinit +import ru.astrainteractive.klibs.kdi.Module +import ru.astrainteractive.klibs.kdi.Single +import ru.astrainteractive.klibs.mikro.platform.PlatformConfiguration + +interface WearRootModule : Module { + val platformConfiguration: Lateinit + val settings: Single + val themeSwitcher: Single + companion object : WearRootModule by WearRootModuleImpl +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt new file mode 100644 index 00000000..3145bdf0 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt @@ -0,0 +1,21 @@ +package com.makeevrserg.empireprojekt.mobile.wear.di.impl + +import com.makeevrserg.empireprojekt.mobile.features.root.di.factories.SettingsFactory +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent +import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import ru.astrainteractive.klibs.kdi.Lateinit +import ru.astrainteractive.klibs.kdi.Single +import ru.astrainteractive.klibs.kdi.getValue +import ru.astrainteractive.klibs.mikro.platform.PlatformConfiguration + +object WearRootModuleImpl : WearRootModule { + override val platformConfiguration: Lateinit = Lateinit() + override val settings = Single { + val configuration by platformConfiguration + SettingsFactory(configuration).create() + } + override val themeSwitcher: Single = Single { + ThemeSwitcherComponent(settings.value) + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/components/AstraChip.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/components/AstraChip.kt new file mode 100644 index 00000000..44d829eb --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/components/AstraChip.kt @@ -0,0 +1,34 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.components + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.RowScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.wear.compose.material.Chip +import androidx.wear.compose.material.ChipDefaults +import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme + +@Composable +fun AstraChip( + label: @Composable RowScope.() -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, + icon: (@Composable BoxScope.() -> Unit)? = null +) { + Chip( + modifier = modifier, + label = label, + onClick = onClick, + icon = icon, + colors = ChipDefaults.chipColors( + backgroundColor = AppTheme.materialColor.primary, + contentColor = AppTheme.materialColor.onPrimary, + secondaryContentColor = AppTheme.materialColor.secondary, + iconColor = AppTheme.materialColor.onPrimary, + disabledBackgroundColor = AppTheme.materialColor.primary.copy(0.5f), + disabledContentColor = AppTheme.materialColor.onPrimary.copy(0.5f), + disabledSecondaryContentColor = AppTheme.materialColor.secondary.copy(0.5f), + disabledIconColor = AppTheme.materialColor.onPrimary.copy(0.5f) + ), + ) +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt new file mode 100644 index 00000000..44cb5470 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt @@ -0,0 +1,46 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.main + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.wear.compose.material.Scaffold +import androidx.wear.compose.material.Vignette +import androidx.wear.compose.material.VignettePosition +import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme +import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import com.makeevrserg.empireprojekt.mobile.wear.features.main.components.NavChip +import com.makeevrserg.empireprojekt.mobile.wear.features.main.components.ThemeChip +import ru.astrainteractive.klibs.kdi.getValue + +@Composable +fun MainScreen(wearRootModule: WearRootModule) { + val themeSwitcher by wearRootModule.themeSwitcher + Scaffold( + modifier = Modifier.background(AppTheme.materialColor.primaryVariant), + vignette = { + Vignette(vignettePosition = VignettePosition.TopAndBottom) + }, + positionIndicator = { + } + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = AppTheme.dimens.M), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(AppTheme.dimens.S)) + ThemeChip(themeSwitcher = themeSwitcher) + Spacer(modifier = Modifier.height(AppTheme.dimens.S)) + NavChip(text = "Statuses") + } + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/NavChip.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/NavChip.kt new file mode 100644 index 00000000..98b0cb3d --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/NavChip.kt @@ -0,0 +1,41 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.main.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.wear.compose.material.ChipDefaults +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.Text +import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme +import com.makeevrserg.empireprojekt.mobile.resources.R +import com.makeevrserg.empireprojekt.mobile.wear.features.components.AstraChip + +@Composable +fun NavChip(text: String) { + AstraChip( + modifier = Modifier.fillMaxWidth(), + label = { + Text( + text = text, + style = AppTheme.typography.caption, + color = AppTheme.materialColor.onPrimary + ) + }, + onClick = {}, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_splash), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .size(ChipDefaults.LargeIconSize) + .wrapContentSize(align = Alignment.Center), + ) + } + ) +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt new file mode 100644 index 00000000..6a931776 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt @@ -0,0 +1,60 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.main.components + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bedtime +import androidx.compose.material.icons.filled.WbSunny +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.wear.compose.material.ChipDefaults +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.Text +import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.wear.features.components.AstraChip + +@Composable +fun ThemeChip(themeSwitcher: ThemeSwitcher) { + val theme by themeSwitcher.theme.collectAsState() + val icon = when (theme) { + ThemeSwitcher.Theme.DARK -> Icons.Filled.Bedtime + ThemeSwitcher.Theme.LIGHT -> Icons.Filled.WbSunny + } + val color by animateColorAsState( + targetValue = when (theme) { + ThemeSwitcher.Theme.DARK -> AppTheme.materialColor.onPrimary + ThemeSwitcher.Theme.LIGHT -> AppTheme.materialColor.onPrimary + }, + label = "LABEL" + ) + AstraChip( + modifier = Modifier.fillMaxWidth(), + label = { + Text( + text = "Switch theme", + style = AppTheme.typography.caption, + color = AppTheme.materialColor.onPrimary + ) + }, + onClick = themeSwitcher::next, + icon = { + Crossfade(targetState = icon, label = "LABEL") { + Icon( + imageVector = it, + contentDescription = null, + modifier = Modifier + .size(ChipDefaults.LargeIconSize) + .wrapContentSize(align = Alignment.Center), + tint = color + ) + } + } + ) +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt new file mode 100644 index 00000000..8f978491 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt @@ -0,0 +1,15 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.main.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.makeevrserg.empireprojekt.mobile.features.ui.root.ComposeApplication +import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import com.makeevrserg.empireprojekt.mobile.wear.features.main.MainScreen + +@Preview +@Composable +private fun RootScreenPreview() { + ComposeApplication { + MainScreen(wearRootModule = WearRootModule) + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt new file mode 100644 index 00000000..cd21b8e1 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt @@ -0,0 +1,43 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.status + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.wear.compose.foundation.lazy.AutoCenteringParams +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.items +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import androidx.wear.compose.material.PositionIndicator +import androidx.wear.compose.material.Scaffold +import androidx.wear.compose.material.Vignette +import androidx.wear.compose.material.VignettePosition +import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import com.makeevrserg.empireprojekt.mobile.wear.features.status.components.StatusWidget + +@Composable +fun StatusesScreen(components: List) { + val listState = rememberScalingLazyListState() + Scaffold( + modifier = Modifier.background(AppTheme.materialColor.primaryVariant), + vignette = { + Vignette(vignettePosition = VignettePosition.TopAndBottom) + }, + positionIndicator = { + PositionIndicator( + scalingLazyListState = listState + ) + } + ) { + ScalingLazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + autoCentering = AutoCenteringParams(itemIndex = 0), + ) { + items(components) { + StatusWidget(it) + } + } + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/components/StatusWidget.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/components/StatusWidget.kt new file mode 100644 index 00000000..f3322d13 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/components/StatusWidget.kt @@ -0,0 +1,61 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.status.components + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.WifiTethering +import androidx.compose.material.icons.filled.WifiTetheringError +import androidx.compose.material.icons.filled.WifiTetheringOff +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.wear.compose.material.ChipDefaults +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.Text +import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import com.makeevrserg.empireprojekt.mobile.wear.features.components.AstraChip + +@Composable +internal fun StatusWidget(component: StatusComponent) { + val model by component.model.collectAsState() + val icon = when (model.status) { + StatusComponent.Model.LoadingStatus.LOADING -> Icons.Filled.WifiTetheringError + StatusComponent.Model.LoadingStatus.SUCCESS -> Icons.Filled.WifiTethering + StatusComponent.Model.LoadingStatus.ERROR -> Icons.Filled.WifiTetheringOff + } + val color by animateColorAsState( + targetValue = when (model.status) { + StatusComponent.Model.LoadingStatus.LOADING -> AppTheme.alColors.astraOrange + StatusComponent.Model.LoadingStatus.SUCCESS -> AppTheme.alColors.colorPositive + StatusComponent.Model.LoadingStatus.ERROR -> AppTheme.alColors.colorNegative + }, + label = "LABEL" + ) + AstraChip( + label = { + Text( + text = "EmpireProjekt.ru", + style = AppTheme.typography.caption, + color = AppTheme.materialColor.onPrimary + ) + }, + onClick = component::checkStatus, + icon = { + Crossfade(targetState = icon, label = "LABEL") { + Icon( + imageVector = it, + contentDescription = null, + modifier = Modifier + .size(ChipDefaults.LargeIconSize) + .wrapContentSize(align = Alignment.Center), + tint = color + ) + } + } + ) +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/preview/StatusesScreenPreview.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/preview/StatusesScreenPreview.kt new file mode 100644 index 00000000..50e140cd --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/preview/StatusesScreenPreview.kt @@ -0,0 +1,18 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.status.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.makeevrserg.empireprojekt.mobile.features.status.StubStatusComponent +import com.makeevrserg.empireprojekt.mobile.features.ui.root.ComposeApplication +import com.makeevrserg.empireprojekt.mobile.wear.features.status.StatusesScreen + +@Preview +@Composable +private fun StatusesScreenPreview() { + val items = List(30) { + StubStatusComponent() + } + ComposeApplication { + StatusesScreen(items) + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/ComposeTileService.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/ComposeTileService.kt new file mode 100644 index 00000000..c77037cd --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/ComposeTileService.kt @@ -0,0 +1,132 @@ +package com.makeevrserg.empireprojekt.mobile.wear.tile + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.GlanceModifier +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.action.actionStartActivity +import androidx.glance.action.clickable +import androidx.glance.layout.Alignment +import androidx.glance.layout.Column +import androidx.glance.layout.ContentScale +import androidx.glance.layout.Spacer +import androidx.glance.layout.height +import androidx.glance.layout.size +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider +import androidx.glance.wear.tiles.GlanceTileService +import androidx.glance.wear.tiles.LocalTimeInterval +import androidx.glance.wear.tiles.TimeInterval +import androidx.glance.wear.tiles.TimelineMode +import androidx.wear.protolayout.ActionBuilders +import androidx.wear.protolayout.DeviceParametersBuilders +import androidx.wear.protolayout.DimensionBuilders +import androidx.wear.protolayout.ModifiersBuilders +import androidx.wear.protolayout.material.ChipColors +import androidx.wear.protolayout.material.TitleChip +import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme +import com.makeevrserg.empireprojekt.mobile.resources.R +import com.makeevrserg.empireprojekt.mobile.wear.MainActivity +import java.time.Instant +import java.util.UUID + +/** + * Just a sample + * + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:glance/glance-wear-tiles/integration-tests/demos/src/main/java/androidx/glance/wear/tiles/demos/CalendarTileService.kt + */ +class ComposeTileService : GlanceTileService() { + private val timeInstant = Instant.now() + override val timelineMode = TimelineMode.TimeBoundEntries( + setOf( + TimeInterval(), + TimeInterval( + timeInstant, + timeInstant.plusSeconds(60) + ), + TimeInterval( + timeInstant.plusSeconds(60), + timeInstant.plusSeconds(120) + ) + ) + ) + + @Composable + private fun Chip(text: String, theme: AppTheme): TitleChip { + val context = LocalContext.current + return TitleChip.Builder( + context, + text, + ModifiersBuilders.Clickable.Builder() + .setId(UUID.randomUUID().toString()) + .setOnClick( + ActionBuilders.LaunchAction.Builder() + .setAndroidActivity( + ActionBuilders.AndroidActivity.Builder() + .setClassName(MainActivity::class.java.name) + .setPackageName(context.packageName) + .build() + ).build() + ).build(), + DeviceParametersBuilders.DeviceParameters.Builder().build() + ).setWidth(DimensionBuilders.expand()).setChipColors( + ChipColors( + theme.materialColor.primaryVariant.colorProp, + theme.materialColor.onPrimary.colorProp, + theme.materialColor.onPrimary.colorProp, + theme.materialColor.primary.colorProp, + ) + ).build() + } + + @Composable + override fun Content() { + val eventTextStyle = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ) + val locationTextStyle = TextStyle( + color = ColorProvider(Color.Gray), + fontSize = 15.sp + ) + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + when (LocalTimeInterval.current) { + timelineMode.timeIntervals.elementAt(0) -> { + Text(text = "No event", style = eventTextStyle) + } + + timelineMode.timeIntervals.elementAt(1) -> { + Text(text = "Coffee", style = eventTextStyle) + Spacer(GlanceModifier.height(5.dp)) + Text(text = "Micro Kitchen", style = locationTextStyle) + } + + timelineMode.timeIntervals.elementAt(2) -> { + Text(text = "Work", style = eventTextStyle) + Spacer(GlanceModifier.height(5.dp)) + Text(text = "Remote from home", style = locationTextStyle) + } + } + + Spacer(GlanceModifier.height(15.dp)) + Chip(text = "Text", theme = AppTheme.DefaultDarkTheme) + Image( + provider = ImageProvider(R.drawable.esmptelegram), + modifier = GlanceModifier + .size(24.dp) + .clickable(actionStartActivity(MainActivity::class.java)), + contentScale = ContentScale.Fit, + contentDescription = "launch calendar activity" + + ) + } + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt new file mode 100644 index 00000000..cf63283f --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt @@ -0,0 +1,190 @@ +package com.makeevrserg.empireprojekt.mobile.wear.tile + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.lifecycleScope +import androidx.wear.protolayout.ActionBuilders +import androidx.wear.protolayout.ColorBuilders +import androidx.wear.protolayout.ColorBuilders.ColorProp +import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters +import androidx.wear.protolayout.DimensionBuilders.dp +import androidx.wear.protolayout.DimensionBuilders.expand +import androidx.wear.protolayout.LayoutElementBuilders +import androidx.wear.protolayout.ModifiersBuilders +import androidx.wear.protolayout.ResourceBuilders +import androidx.wear.protolayout.TimelineBuilders +import androidx.wear.protolayout.material.ChipColors +import androidx.wear.protolayout.material.Text +import androidx.wear.protolayout.material.TitleChip +import androidx.wear.protolayout.material.Typography +import androidx.wear.protolayout.material.layouts.PrimaryLayout +import androidx.wear.tiles.RequestBuilders +import androidx.wear.tiles.TileBuilders +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.compose.tools.LayoutRootPreview +import com.google.android.horologist.compose.tools.buildDeviceParameters +import com.google.android.horologist.tiles.SuspendingTileService +import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.resources.R +import com.makeevrserg.empireprojekt.mobile.wear.MainActivity +import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import ru.astrainteractive.klibs.kdi.getValue + +private const val RESOURCES_VERSION = "1" + +/** + * Skeleton for a tile with no images. + */ +@OptIn(ExperimentalHorologistApi::class) +class MainTileService : SuspendingTileService() { + + private val rootModule by WearRootModule + private val themeSwitcher by rootModule.themeSwitcher + + private val appTheme: AppTheme + get() = when (themeSwitcher.theme.value) { + ThemeSwitcher.Theme.DARK -> AppTheme.DefaultDarkTheme + ThemeSwitcher.Theme.LIGHT -> AppTheme.DefaultLightTheme + } + + override fun onCreate() { + super.onCreate() + lifecycleScope.launch { + while (isActive) { + delay(1000L) + } + } + } + + override suspend fun resourcesRequest( + requestParams: RequestBuilders.ResourcesRequest + ): ResourceBuilders.Resources { + return ResourceBuilders.Resources.Builder() + .setVersion(RESOURCES_VERSION) + .addIdToImageMapping( + R.drawable.esmptelegram::class.simpleName!!, + ResourceBuilders.ImageResource.Builder() + .setAndroidResourceByResId( + ResourceBuilders.AndroidImageResourceByResId.Builder() + .setResourceId(R.drawable.esmptelegram) + .build() + ).build() + ) + .build() + } + + override suspend fun tileRequest( + requestParams: RequestBuilders.TileRequest + ): TileBuilders.Tile { + val layout = LayoutElementBuilders.Layout.Builder() + .setRoot(tileLayout(this, appTheme)) + .build() + val entry = TimelineBuilders.TimelineEntry.Builder() + .setLayout(layout) + .build() + val singleTileTimeline = TimelineBuilders.Timeline.Builder() + .addTimelineEntry(entry) + .build() + + return TileBuilders.Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setFreshnessIntervalMillis(1000L) + .setTileTimeline(singleTileTimeline) + .build() + } +} + +val Color.colorProp: ColorBuilders.ColorProp + get() = ColorProp.Builder() + .setArgb(this.toArgb()) + .build() + +private var lastTimeUpdated = System.currentTimeMillis() +private var prevTimeUpdated = lastTimeUpdated + +private fun tileLayout(context: Context, theme: AppTheme): LayoutElementBuilders.LayoutElement { + prevTimeUpdated = lastTimeUpdated + lastTimeUpdated = System.currentTimeMillis() + val image = LayoutElementBuilders.Image.Builder() + .setWidth(dp(24f)) + .setHeight(dp(24f)) + .setResourceId(R.drawable.esmptelegram::class.simpleName!!) + .setModifiers( + ModifiersBuilders.Modifiers.Builder() + .setSemantics( + ModifiersBuilders.Semantics.Builder() + .setContentDescription("Image description") + .build() + ) + .build() + ).build() + + val text = Text.Builder(context, "Empire Network Status") + .setTypography(Typography.TYPOGRAPHY_CAPTION1) + .setColor(theme.materialColor.onPrimary.colorProp) + .setModifiers( + ModifiersBuilders.Modifiers.Builder() + .setClickable( + ModifiersBuilders.Clickable.Builder() + .setId("reload") + .setOnClick(ActionBuilders.LoadAction.Builder().build()) + .build() + ).build() + ) + .build() + + val titleChip = TitleChip.Builder( + context, + "Open statuses", + ModifiersBuilders.Clickable.Builder() + .setId("openmain") + .setOnClick( + ActionBuilders.LaunchAction.Builder() + .setAndroidActivity( + ActionBuilders.AndroidActivity.Builder() + .setClassName(MainActivity::class.java.name) + .setPackageName(context.packageName) + .build() + ).build() + ).build(), + DeviceParameters.Builder().build() + ).setWidth(expand()).setChipColors( + ChipColors( + theme.materialColor.primaryVariant.colorProp, + theme.materialColor.onPrimary.colorProp, + theme.materialColor.onPrimary.colorProp, + theme.materialColor.primary.colorProp, + ) + ).build() + + val column = LayoutElementBuilders.Column.Builder() + .setWidth(expand()) + .addContent(image) + .addContent(text) + .addContent(titleChip) + .build() + + return PrimaryLayout.Builder(buildDeviceParameters(context.resources)) + .setContent(column) + .build() +} + +@Preview( + device = Devices.WEAR_OS_SMALL_ROUND, + showSystemUi = true, + backgroundColor = 0xff000000, + showBackground = true +) +@Composable +fun TilePreview() { + LayoutRootPreview(root = tileLayout(LocalContext.current, AppTheme.DefaultDarkTheme)) +} From a67ab1017486e0621056c59f3bd4a681a1ef72ea Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Tue, 15 Aug 2023 17:33:07 +0300 Subject: [PATCH 02/20] feat: watchos navigation - added simple navigation --- wearApp/build.gradle.kts | 1 + .../empireprojekt/mobile/wear/MainActivity.kt | 8 +++++-- .../mobile/wear/features/main/MainScreen.kt | 13 +++++++++-- .../wear/features/main/components/NavChip.kt | 4 ++-- .../main/preview/RootScreenPreview.kt | 6 ++++- .../navigation/NavHostRootComponent.kt | 10 ++++++++ .../features/navigation/NavigationScreen.kt | 23 +++++++++++++++++++ .../wear/features/navigation/RootComponent.kt | 9 ++++++++ .../wear/features/status/StatusesScreen.kt | 10 ++++++++ 9 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavHostRootComponent.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavigationScreen.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/RootComponent.kt diff --git a/wearApp/build.gradle.kts b/wearApp/build.gradle.kts index 4caf6ea0..9e440dd3 100644 --- a/wearApp/build.gradle.kts +++ b/wearApp/build.gradle.kts @@ -97,6 +97,7 @@ dependencies { implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.ui:ui-tooling") implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.wear.compose:compose-navigation:1.2.0") implementation("androidx.glance:glance-wear-tiles:1.0.0-alpha05") diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt index 47bb89d5..acddb54a 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt @@ -4,9 +4,11 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.view.WindowCompat +import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import com.makeevrserg.empireprojekt.mobile.features.ui.root.ComposeApplication import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule -import com.makeevrserg.empireprojekt.mobile.wear.features.main.MainScreen +import com.makeevrserg.empireprojekt.mobile.wear.features.navigation.NavHostRootComponent +import com.makeevrserg.empireprojekt.mobile.wear.features.navigation.NavigationScreen import ru.astrainteractive.klibs.kdi.getValue class MainActivity : ComponentActivity() { @@ -16,8 +18,10 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setTheme(com.makeevrserg.empireprojekt.mobile.resources.R.style.AppTheme) setContent { + val navController = rememberSwipeDismissableNavController() + val navHostRootComponent = NavHostRootComponent(navController) ComposeApplication(rootModule.themeSwitcher.value) { - MainScreen(wearRootModule = WearRootModule) + NavigationScreen(navHostRootComponent) } } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt index 44cb5470..8c651732 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt @@ -17,10 +17,14 @@ import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule import com.makeevrserg.empireprojekt.mobile.wear.features.main.components.NavChip import com.makeevrserg.empireprojekt.mobile.wear.features.main.components.ThemeChip +import com.makeevrserg.empireprojekt.mobile.wear.features.navigation.NavHostRootComponent import ru.astrainteractive.klibs.kdi.getValue @Composable -fun MainScreen(wearRootModule: WearRootModule) { +fun MainScreen( + wearRootModule: WearRootModule, + rootComponent: NavHostRootComponent +) { val themeSwitcher by wearRootModule.themeSwitcher Scaffold( modifier = Modifier.background(AppTheme.materialColor.primaryVariant), @@ -40,7 +44,12 @@ fun MainScreen(wearRootModule: WearRootModule) { Spacer(modifier = Modifier.height(AppTheme.dimens.S)) ThemeChip(themeSwitcher = themeSwitcher) Spacer(modifier = Modifier.height(AppTheme.dimens.S)) - NavChip(text = "Statuses") + NavChip( + text = "Statuses", + onClick = { + rootComponent.openStatuses() + } + ) } } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/NavChip.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/NavChip.kt index 98b0cb3d..b011235b 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/NavChip.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/NavChip.kt @@ -16,7 +16,7 @@ import com.makeevrserg.empireprojekt.mobile.resources.R import com.makeevrserg.empireprojekt.mobile.wear.features.components.AstraChip @Composable -fun NavChip(text: String) { +fun NavChip(text: String, onClick: () -> Unit) { AstraChip( modifier = Modifier.fillMaxWidth(), label = { @@ -26,7 +26,7 @@ fun NavChip(text: String) { color = AppTheme.materialColor.onPrimary ) }, - onClick = {}, + onClick = onClick, icon = { Icon( painter = painterResource(id = R.drawable.ic_splash), diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt index 8f978491..f83384d1 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt @@ -2,14 +2,18 @@ package com.makeevrserg.empireprojekt.mobile.wear.features.main.preview import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import com.makeevrserg.empireprojekt.mobile.features.ui.root.ComposeApplication import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule import com.makeevrserg.empireprojekt.mobile.wear.features.main.MainScreen +import com.makeevrserg.empireprojekt.mobile.wear.features.navigation.NavHostRootComponent @Preview @Composable private fun RootScreenPreview() { + val navController = rememberSwipeDismissableNavController() + val navHostRootComponent = NavHostRootComponent(navController) ComposeApplication { - MainScreen(wearRootModule = WearRootModule) + MainScreen(wearRootModule = WearRootModule, rootComponent = navHostRootComponent) } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavHostRootComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavHostRootComponent.kt new file mode 100644 index 00000000..4290df20 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavHostRootComponent.kt @@ -0,0 +1,10 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.navigation + +import androidx.navigation.NavHostController + +class NavHostRootComponent(val navController: NavHostController) : RootComponent { + + override fun openStatuses() { + navController.navigate(RootComponent.Child.Statuses::class.simpleName!!) + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavigationScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavigationScreen.kt new file mode 100644 index 00000000..d8f8714f --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavigationScreen.kt @@ -0,0 +1,23 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.navigation + +import androidx.compose.runtime.Composable +import androidx.wear.compose.navigation.SwipeDismissableNavHost +import androidx.wear.compose.navigation.composable +import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import com.makeevrserg.empireprojekt.mobile.wear.features.main.MainScreen +import com.makeevrserg.empireprojekt.mobile.wear.features.status.StatusesScreen + +@Composable +fun NavigationScreen(rootComponent: NavHostRootComponent) { + SwipeDismissableNavHost( + navController = rootComponent.navController, + startDestination = RootComponent.Child.Main::class.simpleName!! + ) { + composable(RootComponent.Child.Main::class.simpleName!!) { + MainScreen(wearRootModule = WearRootModule, rootComponent = rootComponent) + } + composable(RootComponent.Child.Statuses::class.simpleName!!) { + StatusesScreen(components = emptyList()) + } + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/RootComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/RootComponent.kt new file mode 100644 index 00000000..778fab07 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/RootComponent.kt @@ -0,0 +1,9 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.navigation + +interface RootComponent { + fun openStatuses() + sealed interface Child { + object Main : Child + object Statuses : Child + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt index cd21b8e1..9500c1d1 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt @@ -10,6 +10,7 @@ import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState import androidx.wear.compose.material.PositionIndicator import androidx.wear.compose.material.Scaffold +import androidx.wear.compose.material.Text import androidx.wear.compose.material.Vignette import androidx.wear.compose.material.VignettePosition import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme @@ -35,6 +36,15 @@ fun StatusesScreen(components: List) { modifier = Modifier.fillMaxSize(), autoCentering = AutoCenteringParams(itemIndex = 0), ) { + if (components.isEmpty()) { + item { + Text( + text = "No items present", + style = AppTheme.typography.caption, + color = AppTheme.materialColor.onPrimary + ) + } + } items(components) { StatusWidget(it) } From 99ad8933cb19b130d937ff482fabe6f918968fca Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Tue, 15 Aug 2023 17:59:49 +0300 Subject: [PATCH 03/20] feat: watchos - added content into list screen --- .../empireprojekt/mobile/wear/di/WearRootModule.kt | 3 +++ .../mobile/wear/di/impl/WearRootModuleImpl.kt | 4 ++++ .../wear/features/navigation/NavigationScreen.kt | 2 +- .../mobile/wear/features/status/StatusesScreen.kt | 7 +++---- .../wear/features/status/WearStatusComponent.kt | 14 ++++++++++++++ .../status/preview/StatusesScreenPreview.kt | 7 ++----- 6 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt index 3e3bb462..111d233e 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt @@ -2,6 +2,7 @@ package com.makeevrserg.empireprojekt.mobile.wear.di import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher import com.makeevrserg.empireprojekt.mobile.wear.di.impl.WearRootModuleImpl +import com.makeevrserg.empireprojekt.mobile.wear.features.status.WearStatusComponent import com.russhwolf.settings.Settings import ru.astrainteractive.klibs.kdi.Lateinit import ru.astrainteractive.klibs.kdi.Module @@ -12,5 +13,7 @@ interface WearRootModule : Module { val platformConfiguration: Lateinit val settings: Single val themeSwitcher: Single + val wearStatusComponent: Single + companion object : WearRootModule by WearRootModuleImpl } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt index 3145bdf0..d4298384 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt @@ -4,6 +4,7 @@ import com.makeevrserg.empireprojekt.mobile.features.root.di.factories.SettingsF import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import com.makeevrserg.empireprojekt.mobile.wear.features.status.WearStatusComponent import ru.astrainteractive.klibs.kdi.Lateinit import ru.astrainteractive.klibs.kdi.Single import ru.astrainteractive.klibs.kdi.getValue @@ -18,4 +19,7 @@ object WearRootModuleImpl : WearRootModule { override val themeSwitcher: Single = Single { ThemeSwitcherComponent(settings.value) } + override val wearStatusComponent: Single = Single { + WearStatusComponent.Stub() + } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavigationScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavigationScreen.kt index d8f8714f..da8a292f 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavigationScreen.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavigationScreen.kt @@ -17,7 +17,7 @@ fun NavigationScreen(rootComponent: NavHostRootComponent) { MainScreen(wearRootModule = WearRootModule, rootComponent = rootComponent) } composable(RootComponent.Child.Statuses::class.simpleName!!) { - StatusesScreen(components = emptyList()) + StatusesScreen(WearRootModule.wearStatusComponent.value) } } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt index 9500c1d1..42ebb899 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt @@ -14,11 +14,10 @@ import androidx.wear.compose.material.Text import androidx.wear.compose.material.Vignette import androidx.wear.compose.material.VignettePosition import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme -import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent import com.makeevrserg.empireprojekt.mobile.wear.features.status.components.StatusWidget @Composable -fun StatusesScreen(components: List) { +fun StatusesScreen(wearStatusComponent: WearStatusComponent) { val listState = rememberScalingLazyListState() Scaffold( modifier = Modifier.background(AppTheme.materialColor.primaryVariant), @@ -36,7 +35,7 @@ fun StatusesScreen(components: List) { modifier = Modifier.fillMaxSize(), autoCentering = AutoCenteringParams(itemIndex = 0), ) { - if (components.isEmpty()) { + if (wearStatusComponent.statuses.isEmpty()) { item { Text( text = "No items present", @@ -45,7 +44,7 @@ fun StatusesScreen(components: List) { ) } } - items(components) { + items(wearStatusComponent.statuses) { StatusWidget(it) } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt new file mode 100644 index 00000000..89220a25 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt @@ -0,0 +1,14 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.status + +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import com.makeevrserg.empireprojekt.mobile.features.status.StubStatusComponent + +interface WearStatusComponent { + val statuses: List + + class Stub : WearStatusComponent { + override val statuses: List = List(10) { + StubStatusComponent() + } + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/preview/StatusesScreenPreview.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/preview/StatusesScreenPreview.kt index 50e140cd..856d7273 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/preview/StatusesScreenPreview.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/preview/StatusesScreenPreview.kt @@ -2,17 +2,14 @@ package com.makeevrserg.empireprojekt.mobile.wear.features.status.preview import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import com.makeevrserg.empireprojekt.mobile.features.status.StubStatusComponent import com.makeevrserg.empireprojekt.mobile.features.ui.root.ComposeApplication import com.makeevrserg.empireprojekt.mobile.wear.features.status.StatusesScreen +import com.makeevrserg.empireprojekt.mobile.wear.features.status.WearStatusComponent @Preview @Composable private fun StatusesScreenPreview() { - val items = List(30) { - StubStatusComponent() - } ComposeApplication { - StatusesScreen(items) + StatusesScreen(WearStatusComponent.Stub()) } } From 99b4b2ae10e693e6f2968e810574785ed0a35dea Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Tue, 15 Aug 2023 19:17:59 +0300 Subject: [PATCH 04/20] feat: watchos - added buttons on tile --- .../wear/features/components/AstraChip.kt | 44 +++++++++ .../wear/features/status/StatusesScreen.kt | 48 ++++++++- .../features/status/WearStatusComponent.kt | 26 +++++ .../mobile/wear/tile/ComposeTileService.kt | 45 ++------- .../mobile/wear/tile/MainTileService.kt | 82 +++++++-------- .../wear/tile/components/DefaultPreview.kt | 12 +++ .../tile/components/StatusesRowFactory.kt | 99 +++++++++++++++++++ 7 files changed, 277 insertions(+), 79 deletions(-) create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/DefaultPreview.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowFactory.kt diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/components/AstraChip.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/components/AstraChip.kt index 44d829eb..3ec76335 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/components/AstraChip.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/components/AstraChip.kt @@ -1,11 +1,19 @@ package com.makeevrserg.empireprojekt.mobile.wear.features.components +import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.wear.compose.material.Chip import androidx.wear.compose.material.ChipDefaults +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.Text import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme @Composable @@ -32,3 +40,39 @@ fun AstraChip( ), ) } + +@Composable +fun IconTextChip( + text: String, + imageVector: ImageVector, + modifier: Modifier = Modifier, + textColor: Color = AppTheme.materialColor.onPrimary, + iconColor: Color = Color.Unspecified, + onClick: () -> Unit = { } +) { + AstraChip( + modifier = modifier, + label = { + Crossfade(targetState = text, label = "LABEL") { text -> + Text( + text = text, + style = AppTheme.typography.caption, + color = textColor + ) + } + }, + onClick = onClick, + icon = { + Crossfade(targetState = imageVector, label = "LABEL") { imageVector -> + Icon( + imageVector = imageVector, + contentDescription = null, + modifier = Modifier + .size(ChipDefaults.LargeIconSize) + .wrapContentSize(align = Alignment.Center), + tint = iconColor + ) + } + } + ) +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt index 42ebb899..79b55f64 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt @@ -1,8 +1,19 @@ package com.makeevrserg.empireprojekt.mobile.wear.features.status import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.WifiTethering +import androidx.compose.material.icons.filled.WifiTetheringError +import androidx.compose.material.icons.filled.WifiTetheringOff import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.wear.compose.foundation.lazy.AutoCenteringParams import androidx.wear.compose.foundation.lazy.ScalingLazyColumn @@ -14,10 +25,13 @@ import androidx.wear.compose.material.Text import androidx.wear.compose.material.Vignette import androidx.wear.compose.material.VignettePosition import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme +import com.makeevrserg.empireprojekt.mobile.wear.features.components.IconTextChip import com.makeevrserg.empireprojekt.mobile.wear.features.status.components.StatusWidget +@OptIn(ExperimentalLayoutApi::class) @Composable fun StatusesScreen(wearStatusComponent: WearStatusComponent) { + val mergedState by wearStatusComponent.mergedState.collectAsState() val listState = rememberScalingLazyListState() Scaffold( modifier = Modifier.background(AppTheme.materialColor.primaryVariant), @@ -43,9 +57,37 @@ fun StatusesScreen(wearStatusComponent: WearStatusComponent) { color = AppTheme.materialColor.onPrimary ) } - } - items(wearStatusComponent.statuses) { - StatusWidget(it) + } else { + item { + FlowRow( + horizontalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy(AppTheme.dimens.XS) + ) { + IconTextChip( + modifier = Modifier.weight(1f), + text = mergedState.successCount.toString(), + imageVector = Icons.Filled.WifiTethering, + iconColor = AppTheme.alColors.colorPositive + ) + Spacer(modifier = Modifier.size(AppTheme.dimens.S)) + IconTextChip( + modifier = Modifier.weight(1f), + text = mergedState.loadingCount.toString(), + imageVector = Icons.Filled.WifiTetheringError, + iconColor = AppTheme.alColors.astraOrange + ) + Spacer(modifier = Modifier.size(AppTheme.dimens.S)) + IconTextChip( + modifier = Modifier.weight(1f), + text = mergedState.failureCount.toString(), + imageVector = Icons.Filled.WifiTetheringOff, + iconColor = AppTheme.alColors.colorNegative + ) + } + } + items(wearStatusComponent.statuses) { + StatusWidget(it) + } } } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt index 89220a25..c5d6bdb0 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt @@ -2,13 +2,39 @@ package com.makeevrserg.empireprojekt.mobile.wear.features.status import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent import com.makeevrserg.empireprojekt.mobile.features.status.StubStatusComponent +import kotlinx.coroutines.flow.StateFlow +import ru.astrainteractive.klibs.mikro.core.util.combineStates interface WearStatusComponent { val statuses: List + val mergedState: StateFlow + + data class Model( + val loadingCount: Int = 0, + val successCount: Int = 0, + val failureCount: Int = 0 + ) class Stub : WearStatusComponent { override val statuses: List = List(10) { StubStatusComponent() } + override val mergedState: StateFlow = combineStates( + *statuses.map { it.model }.toTypedArray(), + transform = { statuses -> + val associated = statuses.map { + if (it.isLoading) { + StatusComponent.Model.LoadingStatus.LOADING + } else { + it.status + } + } + Model( + loadingCount = associated.count { it == StatusComponent.Model.LoadingStatus.LOADING }, + successCount = associated.count { it == StatusComponent.Model.LoadingStatus.SUCCESS }, + failureCount = associated.count { it == StatusComponent.Model.LoadingStatus.ERROR } + ) + } + ) } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/ComposeTileService.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/ComposeTileService.kt index c77037cd..355d65e2 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/ComposeTileService.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/ComposeTileService.kt @@ -1,13 +1,14 @@ package com.makeevrserg.empireprojekt.mobile.wear.tile +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.glance.GlanceId import androidx.glance.GlanceModifier import androidx.glance.Image import androidx.glance.ImageProvider -import androidx.glance.LocalContext import androidx.glance.action.actionStartActivity import androidx.glance.action.clickable import androidx.glance.layout.Alignment @@ -24,17 +25,10 @@ import androidx.glance.wear.tiles.GlanceTileService import androidx.glance.wear.tiles.LocalTimeInterval import androidx.glance.wear.tiles.TimeInterval import androidx.glance.wear.tiles.TimelineMode -import androidx.wear.protolayout.ActionBuilders -import androidx.wear.protolayout.DeviceParametersBuilders -import androidx.wear.protolayout.DimensionBuilders -import androidx.wear.protolayout.ModifiersBuilders -import androidx.wear.protolayout.material.ChipColors -import androidx.wear.protolayout.material.TitleChip -import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme +import androidx.glance.wear.tiles.action.ActionCallback import com.makeevrserg.empireprojekt.mobile.resources.R import com.makeevrserg.empireprojekt.mobile.wear.MainActivity import java.time.Instant -import java.util.UUID /** * Just a sample @@ -57,32 +51,11 @@ class ComposeTileService : GlanceTileService() { ) ) - @Composable - private fun Chip(text: String, theme: AppTheme): TitleChip { - val context = LocalContext.current - return TitleChip.Builder( - context, - text, - ModifiersBuilders.Clickable.Builder() - .setId(UUID.randomUUID().toString()) - .setOnClick( - ActionBuilders.LaunchAction.Builder() - .setAndroidActivity( - ActionBuilders.AndroidActivity.Builder() - .setClassName(MainActivity::class.java.name) - .setPackageName(context.packageName) - .build() - ).build() - ).build(), - DeviceParametersBuilders.DeviceParameters.Builder().build() - ).setWidth(DimensionBuilders.expand()).setChipColors( - ChipColors( - theme.materialColor.primaryVariant.colorProp, - theme.materialColor.onPrimary.colorProp, - theme.materialColor.onPrimary.colorProp, - theme.materialColor.primary.colorProp, - ) - ).build() + object EmptyAction : ActionCallback { + override suspend fun onAction( + context: Context, + glanceId: GlanceId + ) = Unit } @Composable @@ -117,7 +90,6 @@ class ComposeTileService : GlanceTileService() { } Spacer(GlanceModifier.height(15.dp)) - Chip(text = "Text", theme = AppTheme.DefaultDarkTheme) Image( provider = ImageProvider(R.drawable.esmptelegram), modifier = GlanceModifier @@ -125,7 +97,6 @@ class ComposeTileService : GlanceTileService() { .clickable(actionStartActivity(MainActivity::class.java)), contentScale = ContentScale.Fit, contentDescription = "launch calendar activity" - ) } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt index cf63283f..6b58a565 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt @@ -5,22 +5,18 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.lifecycleScope import androidx.wear.protolayout.ActionBuilders import androidx.wear.protolayout.ColorBuilders import androidx.wear.protolayout.ColorBuilders.ColorProp -import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters import androidx.wear.protolayout.DimensionBuilders.dp import androidx.wear.protolayout.DimensionBuilders.expand import androidx.wear.protolayout.LayoutElementBuilders +import androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER import androidx.wear.protolayout.ModifiersBuilders import androidx.wear.protolayout.ResourceBuilders import androidx.wear.protolayout.TimelineBuilders -import androidx.wear.protolayout.material.ChipColors import androidx.wear.protolayout.material.Text -import androidx.wear.protolayout.material.TitleChip import androidx.wear.protolayout.material.Typography import androidx.wear.protolayout.material.layouts.PrimaryLayout import androidx.wear.tiles.RequestBuilders @@ -34,6 +30,9 @@ import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher import com.makeevrserg.empireprojekt.mobile.resources.R import com.makeevrserg.empireprojekt.mobile.wear.MainActivity import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import com.makeevrserg.empireprojekt.mobile.wear.features.status.WearStatusComponent +import com.makeevrserg.empireprojekt.mobile.wear.tile.components.DefaultPreview +import com.makeevrserg.empireprojekt.mobile.wear.tile.components.StatusesRowFactory import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -49,6 +48,7 @@ class MainTileService : SuspendingTileService() { private val rootModule by WearRootModule private val themeSwitcher by rootModule.themeSwitcher + private val wearStatusComponent by rootModule.wearStatusComponent private val appTheme: AppTheme get() = when (themeSwitcher.theme.value) { @@ -86,7 +86,7 @@ class MainTileService : SuspendingTileService() { requestParams: RequestBuilders.TileRequest ): TileBuilders.Tile { val layout = LayoutElementBuilders.Layout.Builder() - .setRoot(tileLayout(this, appTheme)) + .setRoot(tileLayout(this, appTheme, wearStatusComponent)) .build() val entry = TimelineBuilders.TimelineEntry.Builder() .setLayout(layout) @@ -111,7 +111,11 @@ val Color.colorProp: ColorBuilders.ColorProp private var lastTimeUpdated = System.currentTimeMillis() private var prevTimeUpdated = lastTimeUpdated -private fun tileLayout(context: Context, theme: AppTheme): LayoutElementBuilders.LayoutElement { +private fun tileLayout( + context: Context, + theme: AppTheme, + wearStatusComponent: WearStatusComponent +): LayoutElementBuilders.LayoutElement { prevTimeUpdated = lastTimeUpdated lastTimeUpdated = System.currentTimeMillis() val image = LayoutElementBuilders.Image.Builder() @@ -142,35 +146,34 @@ private fun tileLayout(context: Context, theme: AppTheme): LayoutElementBuilders ) .build() - val titleChip = TitleChip.Builder( - context, - "Open statuses", - ModifiersBuilders.Clickable.Builder() - .setId("openmain") - .setOnClick( - ActionBuilders.LaunchAction.Builder() - .setAndroidActivity( - ActionBuilders.AndroidActivity.Builder() - .setClassName(MainActivity::class.java.name) - .setPackageName(context.packageName) - .build() - ).build() - ).build(), - DeviceParameters.Builder().build() - ).setWidth(expand()).setChipColors( - ChipColors( - theme.materialColor.primaryVariant.colorProp, - theme.materialColor.onPrimary.colorProp, - theme.materialColor.onPrimary.colorProp, - theme.materialColor.primary.colorProp, - ) - ).build() - val column = LayoutElementBuilders.Column.Builder() + .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER) .setWidth(expand()) .addContent(image) .addContent(text) - .addContent(titleChip) + .addContent( + StatusesRowFactory( + context = context, + wearStatusComponent = wearStatusComponent, + theme = theme + ).create() + ) + .setModifiers( + ModifiersBuilders.Modifiers.Builder() + .setClickable( + ModifiersBuilders.Clickable.Builder() + .setId("openmain") + .setOnClick( + ActionBuilders.LaunchAction.Builder() + .setAndroidActivity( + ActionBuilders.AndroidActivity.Builder() + .setClassName(MainActivity::class.java.name) + .setPackageName(context.packageName) + .build() + ).build() + ).build() + ).build() + ) .build() return PrimaryLayout.Builder(buildDeviceParameters(context.resources)) @@ -178,13 +181,14 @@ private fun tileLayout(context: Context, theme: AppTheme): LayoutElementBuilders .build() } -@Preview( - device = Devices.WEAR_OS_SMALL_ROUND, - showSystemUi = true, - backgroundColor = 0xff000000, - showBackground = true -) +@DefaultPreview @Composable fun TilePreview() { - LayoutRootPreview(root = tileLayout(LocalContext.current, AppTheme.DefaultDarkTheme)) + LayoutRootPreview( + root = tileLayout( + LocalContext.current, + AppTheme.DefaultDarkTheme, + WearStatusComponent.Stub() + ) + ) } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/DefaultPreview.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/DefaultPreview.kt new file mode 100644 index 00000000..d41c1516 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/DefaultPreview.kt @@ -0,0 +1,12 @@ +package com.makeevrserg.empireprojekt.mobile.wear.tile.components + +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview + +@Preview( + device = Devices.WEAR_OS_SMALL_ROUND, + showSystemUi = true, + backgroundColor = 0xff000000, + showBackground = true +) +annotation class DefaultPreview diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowFactory.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowFactory.kt new file mode 100644 index 00000000..f4d1b9ca --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowFactory.kt @@ -0,0 +1,99 @@ +package com.makeevrserg.empireprojekt.mobile.wear.tile.components + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.wear.protolayout.ActionBuilders +import androidx.wear.protolayout.DimensionBuilders +import androidx.wear.protolayout.LayoutElementBuilders +import androidx.wear.protolayout.ModifiersBuilders +import androidx.wear.protolayout.material.Button +import androidx.wear.protolayout.material.ButtonColors +import com.google.android.horologist.compose.tools.LayoutRootPreview +import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme +import com.makeevrserg.empireprojekt.mobile.wear.features.status.WearStatusComponent +import ru.astrainteractive.klibs.kdi.Factory +import java.util.UUID + +class StatusesRowFactory( + private val context: Context, + private val wearStatusComponent: WearStatusComponent, + private val theme: AppTheme +) : Factory { + + private fun statusButton( + context: Context, + amount: Int, + theme: AppTheme, + accentColor: Color, + ): Button { + return Button.Builder( + context, + ModifiersBuilders.Clickable.Builder() + .setId(UUID.randomUUID().toString()) + .setOnClick(ActionBuilders.LoadAction.Builder().build()) + .build() + ) + .setButtonColors( + ButtonColors( + theme.materialColor.primary.toArgb(), + accentColor.toArgb() + ) + ) + .setTextContent(amount.toString()).build() + } + + override fun create(): LayoutElementBuilders.Row { + return LayoutElementBuilders.Row.Builder() + .setWidth(DimensionBuilders.wrap()) + .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER) + .addContent( + statusButton( + context = context, + amount = wearStatusComponent.mergedState.value.successCount, + accentColor = theme.alColors.colorPositive, + theme = theme + ) + ) + .addContent( + LayoutElementBuilders.Box.Builder().setWidth(DimensionBuilders.dp(8f)).setHeight( + DimensionBuilders.expand() + ).build() + ) + .addContent( + statusButton( + context = context, + amount = wearStatusComponent.mergedState.value.loadingCount, + accentColor = theme.alColors.astraOrange, + theme = theme + ) + ) + .addContent( + LayoutElementBuilders.Box.Builder().setWidth(DimensionBuilders.dp(8f)).setHeight( + DimensionBuilders.expand() + ).build() + ) + .addContent( + statusButton( + context = context, + amount = wearStatusComponent.mergedState.value.failureCount, + accentColor = theme.alColors.colorNegative, + theme = theme + ) + ).build() + } +} + +@DefaultPreview +@Composable +private fun StatusesComponentFactoryPreview() { + LayoutRootPreview( + root = StatusesRowFactory( + LocalContext.current, + WearStatusComponent.Stub(), + AppTheme.DefaultDarkTheme + ).create() + ) +} From e3dcc2d488927fe1937dd9fe048b0345d0467440 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Thu, 17 Aug 2023 17:51:36 +0300 Subject: [PATCH 05/20] feat: watchos - added renderers --- androidApp/build.gradle.kts | 14 +- .../empireprojekt/mobile/application/App.kt | 8 ++ wearApp/build.gradle.kts | 2 + wearApp/src/main/AndroidManifest.xml | 1 + .../mobile/wear/tile/MainTileService.kt | 124 ++---------------- .../wear/tile/components/MainTileRenderer.kt | 109 +++++++++++++++ ...esRowFactory.kt => StatusesRowRenderer.kt} | 67 ++++------ 7 files changed, 167 insertions(+), 158 deletions(-) create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/MainTileRenderer.kt rename wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/{StatusesRowFactory.kt => StatusesRowRenderer.kt} (52%) diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 3b40e3ab..1ced56ea 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -32,9 +32,15 @@ android { } signingConfigs { - val secretKeyAlias = runCatching { secretProperty("KEY_ALIAS").string }.getOrNull() ?: "" - val secretKeyPassword = runCatching { secretProperty("KEY_PASSWORD").string }.getOrNull() ?: "" - val secretStorePassword = runCatching { secretProperty("STORE_PASSWORD").string }.getOrNull() ?: "" + val secretKeyAlias = runCatching { + secretProperty("KEY_ALIAS").string + }.getOrNull() ?: "" + val secretKeyPassword = runCatching { + secretProperty("KEY_PASSWORD").string + }.getOrNull() ?: "" + val secretStorePassword = runCatching { + secretProperty("STORE_PASSWORD").string + }.getOrNull() ?: "" getByName("debug") { if (file("keystore.jks").exists()) { keyAlias = secretKeyAlias @@ -119,6 +125,8 @@ dependencies { implementation(libs.decompose.compose.jetpack) implementation(libs.decompose.android) implementation("com.google.android.gms:play-services-wearable:18.0.0") + // wear + implementation("com.google.android.horologist:horologist-datalayer:0.5.3") // Local implementation(projects.modules.features.root) implementation(projects.modules.features.ui) diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt index 137701e9..01c9c60e 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt @@ -1,15 +1,19 @@ package com.makeevrserg.empireprojekt.mobile.application import android.app.Application +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.data.WearDataLayerRegistry import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.initialize import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule +import kotlinx.coroutines.MainScope import ru.astrainteractive.klibs.kdi.getValue import ru.astrainteractive.klibs.mikro.platform.DefaultAndroidPlatformConfiguration class App : Application() { private val servicesModule by RootModule.servicesModule + @OptIn(ExperimentalHorologistApi::class) override fun onCreate() { super.onCreate() Firebase.initialize(this) @@ -18,5 +22,9 @@ class App : Application() { applicationContext ) ) + val wearDataLayerRegistry = WearDataLayerRegistry.fromContext( + application = applicationContext, + coroutineScope = MainScope() + ) } } diff --git a/wearApp/build.gradle.kts b/wearApp/build.gradle.kts index 9e440dd3..4f6e47a4 100644 --- a/wearApp/build.gradle.kts +++ b/wearApp/build.gradle.kts @@ -106,6 +106,8 @@ dependencies { implementation("com.google.android.horologist:horologist-compose-tools:0.5.3") implementation("com.google.android.horologist:horologist-tiles:0.5.3") implementation("androidx.wear.watchface:watchface-complications-data-source-ktx:1.1.1") + implementation("com.google.android.horologist:horologist-datalayer-watch:0.5.3") + implementation("com.google.android.horologist:horologist-datalayer-phone:0.5.3") // klibs implementation(libs.klibs.mikro.core) implementation(libs.klibs.mikro.platform) diff --git a/wearApp/src/main/AndroidManifest.xml b/wearApp/src/main/AndroidManifest.xml index dc17e24a..75f5ff76 100644 --- a/wearApp/src/main/AndroidManifest.xml +++ b/wearApp/src/main/AndroidManifest.xml @@ -39,6 +39,7 @@ android:roundIcon="@mipmap/ic_launcher"> + AppTheme.DefaultDarkTheme - ThemeSwitcher.Theme.LIGHT -> AppTheme.DefaultLightTheme - } + private val mainTileRenderer by Single { + MainTileRenderer(applicationContext) + } override fun onCreate() { super.onCreate() + lifecycleScope.launch { while (isActive) { delay(1000L) + TileService.getUpdater(applicationContext).requestUpdate(MainTileService::class.java) } } } @@ -85,8 +67,9 @@ class MainTileService : SuspendingTileService() { override suspend fun tileRequest( requestParams: RequestBuilders.TileRequest ): TileBuilders.Tile { + val state = wearStatusComponent.mergedState.value val layout = LayoutElementBuilders.Layout.Builder() - .setRoot(tileLayout(this, appTheme, wearStatusComponent)) + .setRoot(mainTileRenderer.renderTile(state, requestParams.deviceConfiguration)) .build() val entry = TimelineBuilders.TimelineEntry.Builder() .setLayout(layout) @@ -94,7 +77,6 @@ class MainTileService : SuspendingTileService() { val singleTileTimeline = TimelineBuilders.Timeline.Builder() .addTimelineEntry(entry) .build() - return TileBuilders.Tile.Builder() .setResourcesVersion(RESOURCES_VERSION) .setFreshnessIntervalMillis(1000L) @@ -108,87 +90,7 @@ val Color.colorProp: ColorBuilders.ColorProp .setArgb(this.toArgb()) .build() -private var lastTimeUpdated = System.currentTimeMillis() -private var prevTimeUpdated = lastTimeUpdated - -private fun tileLayout( - context: Context, - theme: AppTheme, - wearStatusComponent: WearStatusComponent -): LayoutElementBuilders.LayoutElement { - prevTimeUpdated = lastTimeUpdated - lastTimeUpdated = System.currentTimeMillis() - val image = LayoutElementBuilders.Image.Builder() - .setWidth(dp(24f)) - .setHeight(dp(24f)) - .setResourceId(R.drawable.esmptelegram::class.simpleName!!) - .setModifiers( - ModifiersBuilders.Modifiers.Builder() - .setSemantics( - ModifiersBuilders.Semantics.Builder() - .setContentDescription("Image description") - .build() - ) - .build() - ).build() - - val text = Text.Builder(context, "Empire Network Status") - .setTypography(Typography.TYPOGRAPHY_CAPTION1) - .setColor(theme.materialColor.onPrimary.colorProp) - .setModifiers( - ModifiersBuilders.Modifiers.Builder() - .setClickable( - ModifiersBuilders.Clickable.Builder() - .setId("reload") - .setOnClick(ActionBuilders.LoadAction.Builder().build()) - .build() - ).build() - ) - .build() - - val column = LayoutElementBuilders.Column.Builder() - .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER) - .setWidth(expand()) - .addContent(image) - .addContent(text) - .addContent( - StatusesRowFactory( - context = context, - wearStatusComponent = wearStatusComponent, - theme = theme - ).create() - ) - .setModifiers( - ModifiersBuilders.Modifiers.Builder() - .setClickable( - ModifiersBuilders.Clickable.Builder() - .setId("openmain") - .setOnClick( - ActionBuilders.LaunchAction.Builder() - .setAndroidActivity( - ActionBuilders.AndroidActivity.Builder() - .setClassName(MainActivity::class.java.name) - .setPackageName(context.packageName) - .build() - ).build() - ).build() - ).build() - ) - .build() - - return PrimaryLayout.Builder(buildDeviceParameters(context.resources)) - .setContent(column) +val Int.asColorProp: ColorBuilders.ColorProp + get() = ColorProp.Builder() + .setArgb(this) .build() -} - -@DefaultPreview -@Composable -fun TilePreview() { - LayoutRootPreview( - root = tileLayout( - LocalContext.current, - AppTheme.DefaultDarkTheme, - WearStatusComponent.Stub() - ) - ) -} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/MainTileRenderer.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/MainTileRenderer.kt new file mode 100644 index 00000000..3bee3d2e --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/MainTileRenderer.kt @@ -0,0 +1,109 @@ +package com.makeevrserg.empireprojekt.mobile.wear.tile.components + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.wear.protolayout.ActionBuilders +import androidx.wear.protolayout.DeviceParametersBuilders +import androidx.wear.protolayout.DimensionBuilders +import androidx.wear.protolayout.LayoutElementBuilders +import androidx.wear.protolayout.ModifiersBuilders +import androidx.wear.protolayout.material.ChipColors +import androidx.wear.protolayout.material.CompactChip +import androidx.wear.protolayout.material.Text +import androidx.wear.protolayout.material.Typography +import androidx.wear.protolayout.material.layouts.PrimaryLayout +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.compose.tools.TileLayoutPreview +import com.google.android.horologist.compose.tools.buildDeviceParameters +import com.google.android.horologist.tiles.render.SingleTileLayoutRenderer +import com.makeevrserg.empireprojekt.mobile.resources.R +import com.makeevrserg.empireprojekt.mobile.wear.MainActivity +import com.makeevrserg.empireprojekt.mobile.wear.features.status.WearStatusComponent +import com.makeevrserg.empireprojekt.mobile.wear.tile.asColorProp +import ru.astrainteractive.klibs.kdi.getValue + +@OptIn(ExperimentalHorologistApi::class) +class MainTileRenderer( + context: Context, +) : SingleTileLayoutRenderer(context) { + private fun tileLayout( + state: WearStatusComponent.Model, + deviceParameters: DeviceParametersBuilders.DeviceParameters + ): LayoutElementBuilders.LayoutElement { + val image = LayoutElementBuilders.Image.Builder() + .setWidth(DimensionBuilders.dp(24f)) + .setHeight(DimensionBuilders.dp(24f)) + .setResourceId(R.drawable.esmptelegram::class.simpleName!!) + .setModifiers( + ModifiersBuilders.Modifiers.Builder() + .setSemantics( + ModifiersBuilders.Semantics.Builder() + .setContentDescription("Image description") + .build() + ) + .build() + ).build() + val text = Text.Builder(context, "Empire Network Status") + .setTypography(Typography.TYPOGRAPHY_CAPTION1) + .setColor(theme.onSurface.asColorProp) + .setModifiers( + ModifiersBuilders.Modifiers.Builder() + .setClickable( + ModifiersBuilders.Clickable.Builder() + .setId("reload") + .setOnClick(ActionBuilders.LoadAction.Builder().build()) + .build() + ).build() + ) + .build() + val compactChip = CompactChip.Builder( + context, + "More", + ModifiersBuilders.Clickable.Builder() + .setId("openmain") + .setOnClick( + ActionBuilders.LaunchAction.Builder() + .setAndroidActivity( + ActionBuilders.AndroidActivity.Builder() + .setClassName(MainActivity::class.java.name) + .setPackageName(context.packageName) + .build() + ).build() + ).build(), + deviceParameters + ).setChipColors(ChipColors(theme.surface, theme.onSurface)).build() + + val statuses = StatusesRowRenderer(context).renderTile(state, deviceParameters) + + val column = LayoutElementBuilders.Column.Builder() + .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER) + .setWidth(DimensionBuilders.expand()) + .addContent(image) + .addContent(text) + .addContent(statuses) + .addContent(compactChip) + .build() + + return PrimaryLayout.Builder(buildDeviceParameters(context.resources)) + .setContent(column) + .build() + } + + override fun renderTile( + state: WearStatusComponent.Model, + deviceParameters: DeviceParametersBuilders.DeviceParameters + ): LayoutElementBuilders.LayoutElement { + return tileLayout(state, deviceParameters) + } +} + +@OptIn(ExperimentalHorologistApi::class) +@DefaultPreview +@Composable +fun TilePreview() { + val context = LocalContext.current + val renderer = remember { MainTileRenderer(context) } + TileLayoutPreview(WearStatusComponent.Model(), WearStatusComponent.Model(), renderer) +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowFactory.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowRenderer.kt similarity index 52% rename from wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowFactory.kt rename to wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowRenderer.kt index f4d1b9ca..c915b06d 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowFactory.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowRenderer.kt @@ -1,60 +1,55 @@ package com.makeevrserg.empireprojekt.mobile.wear.tile.components import android.content.Context -import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext import androidx.wear.protolayout.ActionBuilders +import androidx.wear.protolayout.DeviceParametersBuilders import androidx.wear.protolayout.DimensionBuilders import androidx.wear.protolayout.LayoutElementBuilders import androidx.wear.protolayout.ModifiersBuilders import androidx.wear.protolayout.material.Button import androidx.wear.protolayout.material.ButtonColors -import com.google.android.horologist.compose.tools.LayoutRootPreview +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.tiles.render.SingleTileLayoutRenderer import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme import com.makeevrserg.empireprojekt.mobile.wear.features.status.WearStatusComponent -import ru.astrainteractive.klibs.kdi.Factory import java.util.UUID -class StatusesRowFactory( - private val context: Context, - private val wearStatusComponent: WearStatusComponent, - private val theme: AppTheme -) : Factory { +@OptIn(ExperimentalHorologistApi::class) +class StatusesRowRenderer( + context: Context, +) : SingleTileLayoutRenderer(context) { private fun statusButton( - context: Context, amount: Int, - theme: AppTheme, accentColor: Color, - ): Button { + ): LayoutElementBuilders.LayoutElement { return Button.Builder( context, ModifiersBuilders.Clickable.Builder() .setId(UUID.randomUUID().toString()) .setOnClick(ActionBuilders.LoadAction.Builder().build()) .build() - ) - .setButtonColors( - ButtonColors( - theme.materialColor.primary.toArgb(), - accentColor.toArgb() - ) + ).setButtonColors( + ButtonColors( + theme.surface, + accentColor.toArgb() ) - .setTextContent(amount.toString()).build() + ).setTextContent(amount.toString()).build() } - override fun create(): LayoutElementBuilders.Row { + override fun renderTile( + state: WearStatusComponent.Model, + deviceParameters: DeviceParametersBuilders.DeviceParameters + ): LayoutElementBuilders.LayoutElement { return LayoutElementBuilders.Row.Builder() .setWidth(DimensionBuilders.wrap()) .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER) .addContent( statusButton( - context = context, - amount = wearStatusComponent.mergedState.value.successCount, - accentColor = theme.alColors.colorPositive, - theme = theme + amount = state.successCount, + accentColor = AppTheme.DefaultDarkTheme.alColors.colorPositive, ) ) .addContent( @@ -64,10 +59,8 @@ class StatusesRowFactory( ) .addContent( statusButton( - context = context, - amount = wearStatusComponent.mergedState.value.loadingCount, - accentColor = theme.alColors.astraOrange, - theme = theme + amount = state.loadingCount, + accentColor = AppTheme.DefaultDarkTheme.alColors.astraOrange, ) ) .addContent( @@ -77,23 +70,9 @@ class StatusesRowFactory( ) .addContent( statusButton( - context = context, - amount = wearStatusComponent.mergedState.value.failureCount, - accentColor = theme.alColors.colorNegative, - theme = theme + amount = state.failureCount, + accentColor = AppTheme.DefaultDarkTheme.alColors.colorNegative, ) ).build() } } - -@DefaultPreview -@Composable -private fun StatusesComponentFactoryPreview() { - LayoutRootPreview( - root = StatusesRowFactory( - LocalContext.current, - WearStatusComponent.Stub(), - AppTheme.DefaultDarkTheme - ).create() - ) -} From 9c7121325e29e0995db4282c8fa18d911675c4ab Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Thu, 17 Aug 2023 17:58:54 +0300 Subject: [PATCH 06/20] refactor: changed root status component --- .../features/root/DefaultRootComponent.kt | 65 ++---------------- .../DefaultMinecraftStatusComponent.kt | 3 +- .../MinecraftStatusComponent.kt | 3 +- .../status/root/DefaultRootStatusComponent.kt | 66 +++++++++++++++++++ .../status/root/RootStatusComponent.kt | 7 ++ .../DefaultUrlStatusComponent.kt} | 15 ++--- .../features/status/url/UrlStatusComponent.kt | 12 ++++ .../features/ui/root/ApplicationContent.kt | 2 +- .../mobile/features/ui/status/StatusScreen.kt | 6 +- .../features/ui/status/widget/StatusWidget.kt | 2 +- 10 files changed, 105 insertions(+), 76 deletions(-) rename modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/{ => mincraft}/DefaultMinecraftStatusComponent.kt (94%) rename modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/{ => mincraft}/MinecraftStatusComponent.kt (79%) create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/DefaultRootStatusComponent.kt create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/RootStatusComponent.kt rename modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/{UrlStatusComponent.kt => url/DefaultUrlStatusComponent.kt} (86%) create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/UrlStatusComponent.kt diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt index a8fabc5f..8c991684 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt @@ -10,18 +10,15 @@ import com.arkivanov.decompose.router.stack.push import com.arkivanov.decompose.router.stack.replaceAll import com.arkivanov.decompose.router.stack.replaceCurrent import com.arkivanov.decompose.value.Value -import com.arkivanov.essenty.instancekeeper.getOrCreate import com.makeevrserg.empireprojekt.mobile.features.logic.splash.SplashComponent import com.makeevrserg.empireprojekt.mobile.features.logic.splash.SplashComponentImpl import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule import com.makeevrserg.empireprojekt.mobile.features.root.di.ServicesModule import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.splash.SplashComponentModuleImpl import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.status.StatusModuleImpl -import com.makeevrserg.empireprojekt.mobile.features.status.DefaultMinecraftStatusComponent -import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent -import com.makeevrserg.empireprojekt.mobile.features.status.UrlStatusComponent +import com.makeevrserg.empireprojekt.mobile.features.status.root.DefaultRootStatusComponent +import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher -import com.makeevrserg.empireprojekt.mobile.services.core.CoroutineFeature class DefaultRootComponent( componentContext: ComponentContext, @@ -52,61 +49,11 @@ class DefaultRootComponent( ) RootComponent.Child.Status -> { - val esmpStatusComponent = UrlStatusComponent( - context = context, - url = "https://empireprojekt.ru", - title = "empireprojekt.ru", - module = StatusModuleImpl(rootModule), - coroutineFeature = context.instanceKeeper.getOrCreate { - CoroutineFeature.Default() - } - ) - val ainteractiveStatusComponent = UrlStatusComponent( - context = context, - url = "https://astrainteractive.ru", - title = "astrainteractive.ru", - module = StatusModuleImpl(rootModule), - coroutineFeature = context.instanceKeeper.getOrCreate { - CoroutineFeature.Default() - } - ) - - val alearnerDevStatusComponent = UrlStatusComponent( - context = context, - url = "http://astralearner.empireprojekt.ru:8083/dictionaries/4/words", - title = "Dev: AstraLearner", - module = StatusModuleImpl(rootModule), - coroutineFeature = context.instanceKeeper.getOrCreate { - CoroutineFeature.Default() - } - ) - - val alearnerProdStatusComponent = UrlStatusComponent( - context = context, - url = "http://astralearner.empireprojekt.ru:8081/dictionaries/4/words", - title = "Prod: AstraLearner", - module = StatusModuleImpl(rootModule), - coroutineFeature = context.instanceKeeper.getOrCreate { - CoroutineFeature.Default() - } - ) - - val smpServerStatus = DefaultMinecraftStatusComponent( - context = context, - title = "Empire SMP", - module = StatusModuleImpl(rootModule), - coroutineFeature = context.instanceKeeper.getOrCreate { - CoroutineFeature.Default() - } - ) Configuration.Status( themeSwitcher = rootModule.themeSwitcher.value, - statusComponents = listOf( - esmpStatusComponent, - ainteractiveStatusComponent, - alearnerDevStatusComponent, - alearnerProdStatusComponent, - smpServerStatus + rootStatusComponent = DefaultRootStatusComponent( + context, + StatusModuleImpl(rootModule) ) ) } @@ -137,7 +84,7 @@ class DefaultRootComponent( ) : Configuration class Status( - val statusComponents: List, + val rootStatusComponent: RootStatusComponent, val themeSwitcher: ThemeSwitcher ) : Configuration } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/DefaultMinecraftStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt similarity index 94% rename from modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/DefaultMinecraftStatusComponent.kt rename to modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt index d7ace7fa..95b70b16 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/DefaultMinecraftStatusComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt @@ -1,6 +1,7 @@ -package com.makeevrserg.empireprojekt.mobile.features.status +package com.makeevrserg.empireprojekt.mobile.features.status.mincraft import com.arkivanov.decompose.ComponentContext +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent import com.makeevrserg.empireprojekt.mobile.features.status.data.MinecraftStatusRepository import com.makeevrserg.empireprojekt.mobile.features.status.di.StatusModule import com.makeevrserg.empireprojekt.mobile.services.core.AnyStateFlow diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/MinecraftStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/MinecraftStatusComponent.kt similarity index 79% rename from modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/MinecraftStatusComponent.kt rename to modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/MinecraftStatusComponent.kt index 35a924d0..e0d182f8 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/MinecraftStatusComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/MinecraftStatusComponent.kt @@ -1,5 +1,6 @@ -package com.makeevrserg.empireprojekt.mobile.features.status +package com.makeevrserg.empireprojekt.mobile.features.status.mincraft +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent import com.makeevrserg.empireprojekt.mobile.features.status.data.model.MinecraftStatusResponse import com.makeevrserg.empireprojekt.mobile.services.core.AnyStateFlow import dev.icerock.moko.resources.desc.StringDesc diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/DefaultRootStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/DefaultRootStatusComponent.kt new file mode 100644 index 00000000..1724fa4f --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/DefaultRootStatusComponent.kt @@ -0,0 +1,66 @@ +package com.makeevrserg.empireprojekt.mobile.features.status.root + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import com.makeevrserg.empireprojekt.mobile.features.status.di.StatusModule +import com.makeevrserg.empireprojekt.mobile.features.status.mincraft.DefaultMinecraftStatusComponent +import com.makeevrserg.empireprojekt.mobile.features.status.url.DefaultUrlStatusComponent +import com.makeevrserg.empireprojekt.mobile.services.core.CoroutineFeature + +class DefaultRootStatusComponent( + context: ComponentContext, + private val statusModule: StatusModule +) : RootStatusComponent, ComponentContext by context { + override val statusComponents: List = buildList { + + DefaultUrlStatusComponent( + context = context, + url = "https://empireprojekt.ru", + title = "empireprojekt.ru", + module = statusModule, + coroutineFeature = context.instanceKeeper.getOrCreate { + CoroutineFeature.Default() + } + ).run(::add) + + DefaultUrlStatusComponent( + context = context, + url = "https://astrainteractive.ru", + title = "astrainteractive.ru", + module = statusModule, + coroutineFeature = context.instanceKeeper.getOrCreate { + CoroutineFeature.Default() + } + ).run(::add) + + DefaultUrlStatusComponent( + context = context, + url = "http://astralearner.empireprojekt.ru:8083/dictionaries/4/words", + title = "Dev: AstraLearner", + module = statusModule, + coroutineFeature = context.instanceKeeper.getOrCreate { + CoroutineFeature.Default() + } + ).run(::add) + + DefaultUrlStatusComponent( + context = context, + url = "http://astralearner.empireprojekt.ru:8081/dictionaries/4/words", + title = "Prod: AstraLearner", + module = statusModule, + coroutineFeature = context.instanceKeeper.getOrCreate { + CoroutineFeature.Default() + } + ).run(::add) + + DefaultMinecraftStatusComponent( + context = context, + title = "Empire SMP", + module = statusModule, + coroutineFeature = context.instanceKeeper.getOrCreate { + CoroutineFeature.Default() + } + ).run(::add) + } +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/RootStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/RootStatusComponent.kt new file mode 100644 index 00000000..49086b4c --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/RootStatusComponent.kt @@ -0,0 +1,7 @@ +package com.makeevrserg.empireprojekt.mobile.features.status.root + +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent + +interface RootStatusComponent { + val statusComponents: List +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/UrlStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/DefaultUrlStatusComponent.kt similarity index 86% rename from modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/UrlStatusComponent.kt rename to modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/DefaultUrlStatusComponent.kt index d1da8290..7925e127 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/UrlStatusComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/DefaultUrlStatusComponent.kt @@ -1,6 +1,7 @@ -package com.makeevrserg.empireprojekt.mobile.features.status +package com.makeevrserg.empireprojekt.mobile.features.status.url import com.arkivanov.decompose.ComponentContext +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent import com.makeevrserg.empireprojekt.mobile.features.status.data.StatusRepository import com.makeevrserg.empireprojekt.mobile.features.status.data.UrlStatusRepository import com.makeevrserg.empireprojekt.mobile.features.status.di.StatusModule @@ -14,27 +15,21 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class UrlStatusComponent( +class DefaultUrlStatusComponent( context: ComponentContext, url: String, title: String, module: StatusModule, private val coroutineFeature: CoroutineFeature -) : StatusComponent, StatusModule by module, ComponentContext by context { +) : UrlStatusComponent, StatusModule by module, ComponentContext by context { private val statusRepository: StatusRepository = UrlStatusRepository( url = url, httpClient = httpClient, dispatchers = dispatchers ) - private data class UrlModel( - override val title: StringDesc, - override val isLoading: Boolean, - override val status: StatusComponent.Model.LoadingStatus - ) : StatusComponent.Model - private val _model = MutableStateFlow( - UrlModel( + UrlStatusComponent.Model( title = StringDesc.Raw(title), isLoading = true, status = StatusComponent.Model.LoadingStatus.LOADING diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/UrlStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/UrlStatusComponent.kt new file mode 100644 index 00000000..02ee175d --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/UrlStatusComponent.kt @@ -0,0 +1,12 @@ +package com.makeevrserg.empireprojekt.mobile.features.status.url + +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import dev.icerock.moko.resources.desc.StringDesc + +interface UrlStatusComponent : StatusComponent { + data class Model( + override val title: StringDesc, + override val isLoading: Boolean, + override val status: StatusComponent.Model.LoadingStatus + ) : StatusComponent.Model +} diff --git a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ApplicationContent.kt b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ApplicationContent.kt index 9464841e..87709fe4 100644 --- a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ApplicationContent.kt +++ b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ApplicationContent.kt @@ -36,7 +36,7 @@ fun ApplicationContent( rootComponent = rootComponent, rootBottomSheetComponent = rootBottomSheetComponent, themeSwitcher = screen.themeSwitcher, - statusComponents = screen.statusComponents + rootStatusComponent = screen.rootStatusComponent ) } } diff --git a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt index bc2bd5c7..17a4024a 100644 --- a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt +++ b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt @@ -20,7 +20,7 @@ import com.makeevrserg.empireprojekt.mobile.core.ui.components.navBarsPadding import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme import com.makeevrserg.empireprojekt.mobile.features.root.DefaultRootComponent import com.makeevrserg.empireprojekt.mobile.features.root.RootBottomSheetComponent -import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher import com.makeevrserg.empireprojekt.mobile.features.ui.status.widget.StatusWidget import com.makeevrserg.empireprojekt.mobile.resources.MR @@ -31,7 +31,7 @@ fun StatusScreen( rootComponent: DefaultRootComponent, rootBottomSheetComponent: RootBottomSheetComponent, themeSwitcher: ThemeSwitcher, - statusComponents: List, + rootStatusComponent: RootStatusComponent, ) { Scaffold( modifier = Modifier, @@ -85,7 +85,7 @@ fun StatusScreen( color = AppTheme.materialColor.onPrimary.copy(alpha = .5f) ) } - items(statusComponents) { + items(rootStatusComponent.statusComponents) { StatusWidget(it) } } diff --git a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/widget/StatusWidget.kt b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/widget/StatusWidget.kt index d1fc71b9..09949224 100644 --- a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/widget/StatusWidget.kt +++ b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/widget/StatusWidget.kt @@ -25,8 +25,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.makeevrserg.empireprojekt.mobile.core.ui.asComposableString import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme -import com.makeevrserg.empireprojekt.mobile.features.status.MinecraftStatusComponent import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import com.makeevrserg.empireprojekt.mobile.features.status.mincraft.MinecraftStatusComponent private const val FADE_DURATION = 1200 From c01349113b79ae68a529e8a8d8cb87bda9581528 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Thu, 17 Aug 2023 19:20:00 +0300 Subject: [PATCH 07/20] feat: messaging between android and wear os --- .../empireprojekt/mobile/application/App.kt | 74 +++++++++++++++++++ .../features/root/DefaultRootComponent.kt | 7 +- .../mobile/features/root/di/RootModule.kt | 3 +- .../root/di/impl/root/RootModuleImpl.kt | 6 ++ .../DefaultMinecraftStatusComponent.kt | 4 +- .../status/root/DefaultRootStatusComponent.kt | 30 ++------ .../status/url/DefaultUrlStatusComponent.kt | 4 +- wearApp/src/main/AndroidManifest.xml | 11 +++ .../wear/features/status/StatusesScreen.kt | 66 ++++++++--------- .../features/status/WearStatusComponent.kt | 34 ++++----- .../wear/service/DataLayerListenerService.kt | 47 ++++++++++++ 11 files changed, 192 insertions(+), 94 deletions(-) create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/service/DataLayerListenerService.kt diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt index 01c9c60e..79a54e3e 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt @@ -1,16 +1,25 @@ package com.makeevrserg.empireprojekt.mobile.application import android.app.Application +import android.util.Log import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.data.WearDataLayerRegistry import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.initialize import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent import kotlinx.coroutines.MainScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await import ru.astrainteractive.klibs.kdi.getValue import ru.astrainteractive.klibs.mikro.platform.DefaultAndroidPlatformConfiguration class App : Application() { + private val rootModule by RootModule private val servicesModule by RootModule.servicesModule @OptIn(ExperimentalHorologistApi::class) @@ -26,5 +35,70 @@ class App : Application() { application = applicationContext, coroutineScope = MainScope() ) + // todo work manager + val messageClient = wearDataLayerRegistry.messageClient + + MainScope().launch { + while (isActive) { + delay(5000L) + kotlin.runCatching { + val nodes = wearDataLayerRegistry.nodeClient.connectedNodes.await() + Log.d(TAG, "Contains ${nodes.size} nodes") + val mapped = rootModule.rootStatusComponent.value.statusComponents.map { + if (it.model.value.isLoading) { + StatusComponent.Model.LoadingStatus.LOADING + } else { + it.model.value.status + } + } + val statuses = buildList { + StatusComponent.Model.LoadingStatus.SUCCESS.let { status -> + status to mapped.count { it == status } + }.run(::add) + StatusComponent.Model.LoadingStatus.ERROR.let { status -> + status to mapped.count { it == status } + }.run(::add) + StatusComponent.Model.LoadingStatus.LOADING.let { status -> + status to mapped.count { it == status } + }.run(::add) + } + nodes.flatMap { node -> + statuses.map { entry -> + async { + messageClient.sendMessage( + node.id, + "/statuses" + entry.first.name, + byteArrayOf(entry.second.toByte()) + ) + } + } + }.awaitAll() + Log.d(TAG, "Sended ") + }.onFailure { + it.printStackTrace() + } +// try { +// val helloWorld = "HelloWorld".toByteArray() +// // Send a message to all nodes in parallel +// nodes.map { node -> +// async { +// Log.d(TAG, "Sending message to: ${node.displayName}") +// messageClient.sendMessage(node.id, START_ACTIVITY_PATH, helloWorld) +// .await() +// } +// }.awaitAll() +// +// Log.d(TAG, "Starting activity requests sent successfully") +// } catch (cancellationException: CancellationException) { +// throw cancellationException +// } catch (exception: Exception) { +// Log.d(TAG, "Starting activity failed: $exception") +// } + } + } + } + + companion object { + private const val TAG = "MainActivity" } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt index 8c991684..6853ac33 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt @@ -15,8 +15,6 @@ import com.makeevrserg.empireprojekt.mobile.features.logic.splash.SplashComponen import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule import com.makeevrserg.empireprojekt.mobile.features.root.di.ServicesModule import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.splash.SplashComponentModuleImpl -import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.status.StatusModuleImpl -import com.makeevrserg.empireprojekt.mobile.features.status.root.DefaultRootStatusComponent import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher @@ -51,10 +49,7 @@ class DefaultRootComponent( RootComponent.Child.Status -> { Configuration.Status( themeSwitcher = rootModule.themeSwitcher.value, - rootStatusComponent = DefaultRootStatusComponent( - context, - StatusModuleImpl(rootModule) - ) + rootStatusComponent = rootModule.rootStatusComponent.value ) } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt index b7abd4f6..ebfafa52 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt @@ -1,6 +1,7 @@ package com.makeevrserg.empireprojekt.mobile.features.root.di import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.root.RootModuleImpl +import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher import com.russhwolf.settings.Settings import kotlinx.coroutines.CoroutineScope @@ -15,6 +16,6 @@ interface RootModule : Module { val dispatchers: Single val mainScope: Single val themeSwitcher: Single - + val rootStatusComponent: Single companion object : RootModule by RootModuleImpl } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt index 8ed66afe..0197e08d 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt @@ -2,6 +2,9 @@ package com.makeevrserg.empireprojekt.mobile.features.root.di.impl.root import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule import com.makeevrserg.empireprojekt.mobile.features.root.di.factories.SettingsFactory +import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.status.StatusModuleImpl +import com.makeevrserg.empireprojekt.mobile.features.status.root.DefaultRootStatusComponent +import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent import kotlinx.coroutines.MainScope @@ -30,4 +33,7 @@ internal object RootModuleImpl : RootModule { override val themeSwitcher: Single = Single { ThemeSwitcherComponent(settings.value) } + override val rootStatusComponent: Single = Single { + DefaultRootStatusComponent(StatusModuleImpl(this)) + } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt index 95b70b16..e64c27d5 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt @@ -1,6 +1,5 @@ package com.makeevrserg.empireprojekt.mobile.features.status.mincraft -import com.arkivanov.decompose.ComponentContext import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent import com.makeevrserg.empireprojekt.mobile.features.status.data.MinecraftStatusRepository import com.makeevrserg.empireprojekt.mobile.features.status.di.StatusModule @@ -15,11 +14,10 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class DefaultMinecraftStatusComponent( - context: ComponentContext, private val module: StatusModule, title: String, private val coroutineFeature: CoroutineFeature -) : StatusComponent, StatusModule by module, ComponentContext by context { +) : StatusComponent, StatusModule by module { private val repository = MinecraftStatusRepository.Default( httpClient = httpClient, dispatchers = dispatchers diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/DefaultRootStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/DefaultRootStatusComponent.kt index 1724fa4f..5f405c06 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/DefaultRootStatusComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/DefaultRootStatusComponent.kt @@ -1,7 +1,5 @@ package com.makeevrserg.empireprojekt.mobile.features.status.root -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.essenty.instancekeeper.getOrCreate import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent import com.makeevrserg.empireprojekt.mobile.features.status.di.StatusModule import com.makeevrserg.empireprojekt.mobile.features.status.mincraft.DefaultMinecraftStatusComponent @@ -9,58 +7,42 @@ import com.makeevrserg.empireprojekt.mobile.features.status.url.DefaultUrlStatus import com.makeevrserg.empireprojekt.mobile.services.core.CoroutineFeature class DefaultRootStatusComponent( - context: ComponentContext, private val statusModule: StatusModule -) : RootStatusComponent, ComponentContext by context { +) : RootStatusComponent { override val statusComponents: List = buildList { DefaultUrlStatusComponent( - context = context, url = "https://empireprojekt.ru", title = "empireprojekt.ru", module = statusModule, - coroutineFeature = context.instanceKeeper.getOrCreate { - CoroutineFeature.Default() - } + coroutineFeature = CoroutineFeature.Default() ).run(::add) DefaultUrlStatusComponent( - context = context, url = "https://astrainteractive.ru", title = "astrainteractive.ru", module = statusModule, - coroutineFeature = context.instanceKeeper.getOrCreate { - CoroutineFeature.Default() - } + coroutineFeature = CoroutineFeature.Default() ).run(::add) DefaultUrlStatusComponent( - context = context, url = "http://astralearner.empireprojekt.ru:8083/dictionaries/4/words", title = "Dev: AstraLearner", module = statusModule, - coroutineFeature = context.instanceKeeper.getOrCreate { - CoroutineFeature.Default() - } + coroutineFeature = CoroutineFeature.Default() ).run(::add) DefaultUrlStatusComponent( - context = context, url = "http://astralearner.empireprojekt.ru:8081/dictionaries/4/words", title = "Prod: AstraLearner", module = statusModule, - coroutineFeature = context.instanceKeeper.getOrCreate { - CoroutineFeature.Default() - } + coroutineFeature = CoroutineFeature.Default() ).run(::add) DefaultMinecraftStatusComponent( - context = context, title = "Empire SMP", module = statusModule, - coroutineFeature = context.instanceKeeper.getOrCreate { - CoroutineFeature.Default() - } + coroutineFeature = CoroutineFeature.Default() ).run(::add) } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/DefaultUrlStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/DefaultUrlStatusComponent.kt index 7925e127..a8909a15 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/DefaultUrlStatusComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/DefaultUrlStatusComponent.kt @@ -1,6 +1,5 @@ package com.makeevrserg.empireprojekt.mobile.features.status.url -import com.arkivanov.decompose.ComponentContext import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent import com.makeevrserg.empireprojekt.mobile.features.status.data.StatusRepository import com.makeevrserg.empireprojekt.mobile.features.status.data.UrlStatusRepository @@ -16,12 +15,11 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class DefaultUrlStatusComponent( - context: ComponentContext, url: String, title: String, module: StatusModule, private val coroutineFeature: CoroutineFeature -) : UrlStatusComponent, StatusModule by module, ComponentContext by context { +) : UrlStatusComponent, StatusModule by module { private val statusRepository: StatusRepository = UrlStatusRepository( url = url, httpClient = httpClient, diff --git a/wearApp/src/main/AndroidManifest.xml b/wearApp/src/main/AndroidManifest.xml index 75f5ff76..a0291326 100644 --- a/wearApp/src/main/AndroidManifest.xml +++ b/wearApp/src/main/AndroidManifest.xml @@ -47,6 +47,17 @@ android:resource="@mipmap/ic_launcher" /> + + + + + + @@ -15,26 +15,20 @@ interface WearStatusComponent { val failureCount: Int = 0 ) + fun update(status: StatusComponent.Model.LoadingStatus, amount: Int) + class Stub : WearStatusComponent { - override val statuses: List = List(10) { - StubStatusComponent() - } - override val mergedState: StateFlow = combineStates( - *statuses.map { it.model }.toTypedArray(), - transform = { statuses -> - val associated = statuses.map { - if (it.isLoading) { - StatusComponent.Model.LoadingStatus.LOADING - } else { - it.status - } + override val statuses: List = emptyList() + private val mutableMergeState = MutableStateFlow(Model()) + override val mergedState: StateFlow = mutableMergeState + override fun update(status: StatusComponent.Model.LoadingStatus, amount: Int) { + mutableMergeState.update { + when (status) { + StatusComponent.Model.LoadingStatus.LOADING -> it.copy(loadingCount = amount) + StatusComponent.Model.LoadingStatus.SUCCESS -> it.copy(successCount = amount) + StatusComponent.Model.LoadingStatus.ERROR -> it.copy(failureCount = amount) } - Model( - loadingCount = associated.count { it == StatusComponent.Model.LoadingStatus.LOADING }, - successCount = associated.count { it == StatusComponent.Model.LoadingStatus.SUCCESS }, - failureCount = associated.count { it == StatusComponent.Model.LoadingStatus.ERROR } - ) } - ) + } } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/service/DataLayerListenerService.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/service/DataLayerListenerService.kt new file mode 100644 index 00000000..da97d7c9 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/service/DataLayerListenerService.kt @@ -0,0 +1,47 @@ +package com.makeevrserg.empireprojekt.mobile.wear.service + +import android.util.Log +import com.google.android.gms.wearable.MessageEvent +import com.google.android.gms.wearable.WearableListenerService +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel + +class DataLayerListenerService : WearableListenerService() { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + + override fun onMessageReceived(messageEvent: MessageEvent) { + super.onMessageReceived(messageEvent) + + kotlin.runCatching { + val statusRaw = messageEvent.path.replace("/statuses", "") + val status = StatusComponent.Model.LoadingStatus.valueOf(statusRaw) + val amount = messageEvent.data.first().toInt() + WearRootModule.wearStatusComponent.value.update(status, amount) + Log.d("DataLayerService", "loadingStatus: $status; amount: $amount") + }.onFailure { + it.printStackTrace() + } + kotlin.runCatching { + Log.d(TAG, "onMessageReceived: ${messageEvent.data.decodeToString()}") + }.onFailure { it.printStackTrace() } + } + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "onCreate: ") + } + + override fun onDestroy() { + super.onDestroy() + Log.d(TAG, "onDestroy: DataLayerListenerService") + scope.cancel() + } + + companion object { + private const val TAG = "DataLayerService" + } +} From c5dadfeb06ae8d84252a63c850fde02ed0f62632 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Thu, 17 Aug 2023 19:23:12 +0300 Subject: [PATCH 08/20] fix: wear - fixed single instance - fixed circular dependencies --- wearApp/build.gradle.kts | 1 - wearApp/src/main/AndroidManifest.xml | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/wearApp/build.gradle.kts b/wearApp/build.gradle.kts index 4f6e47a4..f8b0f52d 100644 --- a/wearApp/build.gradle.kts +++ b/wearApp/build.gradle.kts @@ -125,5 +125,4 @@ dependencies { implementation(projects.modules.features.ui) implementation(projects.modules.services.coreUi) implementation(projects.modules.services.resources) - wearApp(project(":wearApp")) } diff --git a/wearApp/src/main/AndroidManifest.xml b/wearApp/src/main/AndroidManifest.xml index a0291326..2f6804b7 100644 --- a/wearApp/src/main/AndroidManifest.xml +++ b/wearApp/src/main/AndroidManifest.xml @@ -89,6 +89,7 @@ android:name=".wear.MainActivity" android:exported="true" android:label="@string/app_name" + android:launchMode="singleInstance" android:theme="@android:style/Theme.DeviceDefault"> From 66674bd61a941627b139f9f9f821e8a6843e5331 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Thu, 17 Aug 2023 19:52:23 +0300 Subject: [PATCH 09/20] feat: worker --- androidApp/build.gradle.kts | 3 + .../empireprojekt/mobile/application/App.kt | 115 +++++++----------- .../mobile/work/CheckStatusWork.kt | 81 ++++++++++++ .../mobile/features/status/StatusComponent.kt | 1 + .../features/status/StubStatusComponent.kt | 24 ++-- .../DefaultMinecraftStatusComponent.kt | 4 +- .../status/url/DefaultUrlStatusComponent.kt | 4 +- 7 files changed, 146 insertions(+), 86 deletions(-) create mode 100644 androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 1ced56ea..76b91509 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -127,6 +127,9 @@ dependencies { implementation("com.google.android.gms:play-services-wearable:18.0.0") // wear implementation("com.google.android.horologist:horologist-datalayer:0.5.3") + // work + implementation("androidx.work:work-runtime:2.8.0") + implementation("androidx.work:work-runtime-ktx:2.8.0") // Local implementation(projects.modules.features.root) implementation(projects.modules.features.ui) diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt index 79a54e3e..c5c23c45 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt @@ -1,28 +1,43 @@ package com.makeevrserg.empireprojekt.mobile.application import android.app.Application -import android.util.Log +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.data.WearDataLayerRegistry import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.initialize import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule -import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll +import com.makeevrserg.empireprojekt.mobile.services.core.CoroutineFeature +import com.makeevrserg.empireprojekt.mobile.work.CheckStatusWork +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.tasks.await import ru.astrainteractive.klibs.kdi.getValue import ru.astrainteractive.klibs.mikro.platform.DefaultAndroidPlatformConfiguration +import java.util.concurrent.TimeUnit +@OptIn(ExperimentalHorologistApi::class) class App : Application() { - private val rootModule by RootModule private val servicesModule by RootModule.servicesModule + private val coroutineFeature = CoroutineFeature.Default() + private val wearDataLayerRegistry by lazy { + WearDataLayerRegistry.fromContext( + application = applicationContext, + coroutineScope = coroutineFeature + ) + } + private val messageClient by lazy { + wearDataLayerRegistry.messageClient + } + + override fun onTerminate() { + super.onTerminate() + coroutineFeature.cancel() + } - @OptIn(ExperimentalHorologistApi::class) override fun onCreate() { super.onCreate() Firebase.initialize(this) @@ -31,74 +46,30 @@ class App : Application() { applicationContext ) ) - val wearDataLayerRegistry = WearDataLayerRegistry.fromContext( - application = applicationContext, - coroutineScope = MainScope() - ) - // todo work manager - val messageClient = wearDataLayerRegistry.messageClient + scheduleWork() + } - MainScope().launch { + private fun scheduleWork() { + val statusWork = PeriodicWorkRequest.Builder( + CheckStatusWork::class.java, + 15, + TimeUnit.MINUTES + ).build() + val instanceWorkManager = WorkManager.getInstance(applicationContext) + instanceWorkManager.enqueueUniquePeriodicWork( + CheckStatusWork::class.java.simpleName, + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + statusWork + ) + coroutineFeature.launch { while (isActive) { delay(5000L) - kotlin.runCatching { - val nodes = wearDataLayerRegistry.nodeClient.connectedNodes.await() - Log.d(TAG, "Contains ${nodes.size} nodes") - val mapped = rootModule.rootStatusComponent.value.statusComponents.map { - if (it.model.value.isLoading) { - StatusComponent.Model.LoadingStatus.LOADING - } else { - it.model.value.status - } - } - val statuses = buildList { - StatusComponent.Model.LoadingStatus.SUCCESS.let { status -> - status to mapped.count { it == status } - }.run(::add) - StatusComponent.Model.LoadingStatus.ERROR.let { status -> - status to mapped.count { it == status } - }.run(::add) - StatusComponent.Model.LoadingStatus.LOADING.let { status -> - status to mapped.count { it == status } - }.run(::add) - } - nodes.flatMap { node -> - statuses.map { entry -> - async { - messageClient.sendMessage( - node.id, - "/statuses" + entry.first.name, - byteArrayOf(entry.second.toByte()) - ) - } - } - }.awaitAll() - Log.d(TAG, "Sended ") - }.onFailure { - it.printStackTrace() - } -// try { -// val helloWorld = "HelloWorld".toByteArray() -// // Send a message to all nodes in parallel -// nodes.map { node -> -// async { -// Log.d(TAG, "Sending message to: ${node.displayName}") -// messageClient.sendMessage(node.id, START_ACTIVITY_PATH, helloWorld) -// .await() -// } -// }.awaitAll() -// -// Log.d(TAG, "Starting activity requests sent successfully") -// } catch (cancellationException: CancellationException) { -// throw cancellationException -// } catch (exception: Exception) { -// Log.d(TAG, "Starting activity failed: $exception") -// } + CheckStatusWork.sendMessageOnWear( + wearDataLayerRegistry = wearDataLayerRegistry, + rootModule = RootModule, + messageClient = messageClient + ) } } } - - companion object { - private const val TAG = "MainActivity" - } } diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt new file mode 100644 index 00000000..96bb22c3 --- /dev/null +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt @@ -0,0 +1,81 @@ +package com.makeevrserg.empireprojekt.mobile.work + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.google.android.gms.wearable.MessageClient +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.data.WearDataLayerRegistry +import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.tasks.await +import ru.astrainteractive.klibs.kdi.getValue + +@OptIn(ExperimentalHorologistApi::class) +class CheckStatusWork( + context: Context, + params: WorkerParameters +) : CoroutineWorker(context, params) { + private val rootModule by RootModule + private val rootStatusComponent by rootModule.rootStatusComponent + + override suspend fun doWork(): Result = coroutineScope { + Log.d(TAG, "doWork: ") + rootStatusComponent.statusComponents.map { + async { + it.checkStatus() + } + }.awaitAll() + Result.success() + } + + companion object { + private const val TAG = "CheckStatusWork" + suspend fun sendMessageOnWear( + wearDataLayerRegistry: WearDataLayerRegistry, + rootModule: RootModule, + messageClient: MessageClient + ) = coroutineScope { + kotlin.runCatching { + val nodes = wearDataLayerRegistry.nodeClient.connectedNodes.await() + Log.d(TAG, "Contains ${nodes.size} nodes") + val mapped = rootModule.rootStatusComponent.value.statusComponents.map { + if (it.model.value.isLoading) { + StatusComponent.Model.LoadingStatus.LOADING + } else { + it.model.value.status + } + } + val statuses = buildList { + StatusComponent.Model.LoadingStatus.SUCCESS.let { status -> + status to mapped.count { it == status } + }.run(::add) + StatusComponent.Model.LoadingStatus.ERROR.let { status -> + status to mapped.count { it == status } + }.run(::add) + StatusComponent.Model.LoadingStatus.LOADING.let { status -> + status to mapped.count { it == status } + }.run(::add) + } + nodes.flatMap { node -> + statuses.map { entry -> + async { + messageClient.sendMessage( + node.id, + "/statuses" + entry.first.name, + byteArrayOf(entry.second.toByte()) + ) + } + } + }.awaitAll() + Log.d(TAG, "Sended ") + }.onFailure { + it.printStackTrace() + } + } + } +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StatusComponent.kt index 01c5d531..36bdb5f9 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StatusComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StatusComponent.kt @@ -6,6 +6,7 @@ import dev.icerock.moko.resources.desc.StringDesc interface StatusComponent { val model: AnyStateFlow fun checkStatus() + suspend fun checkOnce(force: Boolean) interface Model { val title: StringDesc val isLoading: Boolean diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StubStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StubStatusComponent.kt index d3d7d50c..84831968 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StubStatusComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StubStatusComponent.kt @@ -27,16 +27,20 @@ class StubStatusComponent : StatusComponent, CoroutineScope by MainScope() { override fun checkStatus() { launch { - mutableStateFlow.update { - it.copy(isLoading = true) - } - delay(500L) - mutableStateFlow.update { - it.copy( - isLoading = false, - status = StatusComponent.Model.LoadingStatus.values().random() - ) - } + checkOnce(false) + } + } + + override suspend fun checkOnce(force: Boolean) { + mutableStateFlow.update { + it.copy(isLoading = true) + } + delay(500L) + mutableStateFlow.update { + it.copy( + isLoading = false, + status = StatusComponent.Model.LoadingStatus.values().random() + ) } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt index e64c27d5..1b72a31b 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt @@ -38,7 +38,7 @@ class DefaultMinecraftStatusComponent( } } - private suspend fun checkOnce(force: Boolean) { + override suspend fun checkOnce(force: Boolean) { if (_model.value.isLoading && !force) return _model.update { it.copy(isLoading = true) @@ -67,6 +67,6 @@ class DefaultMinecraftStatusComponent( } companion object { - private const val DELAY = 5000L + private const val DELAY = 30000L } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/DefaultUrlStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/DefaultUrlStatusComponent.kt index a8909a15..a43314d6 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/DefaultUrlStatusComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/url/DefaultUrlStatusComponent.kt @@ -40,7 +40,7 @@ class DefaultUrlStatusComponent( } } - private suspend fun checkOnce(force: Boolean) { + override suspend fun checkOnce(force: Boolean) { if (_model.value.isLoading && !force) return _model.update { it.copy(isLoading = true) @@ -75,6 +75,6 @@ class DefaultUrlStatusComponent( } companion object { - private const val DELAY = 5000L + private const val DELAY = 30000L } } From d74efb0cd40fc28b3e6d476f749e71fe30142fc6 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Fri, 18 Aug 2023 20:30:40 +0300 Subject: [PATCH 10/20] fix: ios build --- gradle/libs.versions.toml | 6 +++--- .../UserInterfaceState.xcuserstate | Bin 130793 -> 123999 bytes .../iosApp/Presentation/Root/RootHolder.swift | 6 +++--- .../iosApp/Presentation/Root/RootView.swift | 2 +- .../Presentation/Status/StatusView.swift | 8 ++++---- modules/features/root/build.gradle.kts | 8 +++++++- .../root/di/factories/SettingsFactory.kt | 3 ++- modules/features/splash/build.gradle.kts | 1 + modules/services/core/build.gradle.kts | 1 + modules/services/resources/build.gradle.kts | 7 +++++++ 10 files changed, 29 insertions(+), 13 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3fbb84ac..a464b644 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] # Kotlin -kotlin-version = "1.8.20" +kotlin-version = "1.9.0" kotlin-dokka = "1.8.10" kotlin-coroutines = "1.7.2" -kotlin-compilerExtensionVersion = "1.4.5" +kotlin-compilerExtensionVersion = "1.5.1" kotlin-android-application = "8.0.1" kotlin-serialization-json = "1.5.1" @@ -55,7 +55,7 @@ klibs-kstorage = "1.0.0" klibs-kdi = "1.1.0" # Compose -kotlin-compose = "1.4.0" +kotlin-compose = "1.4.3" # Moko moko-mvvm = "0.16.1" diff --git a/iosApp/iosApp.xcworkspace/xcuserdata/romanmakeev.xcuserdatad/UserInterfaceState.xcuserstate b/iosApp/iosApp.xcworkspace/xcuserdata/romanmakeev.xcuserdatad/UserInterfaceState.xcuserstate index 68d720e0594e4da28ad1ffcb6ea0961b19a72fa3..8b901272ec9cfd53e4b0a3dbabbb0a6ae3240695 100644 GIT binary patch literal 123999 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 diff --git a/iosApp/iosApp/Presentation/Root/RootHolder.swift b/iosApp/iosApp/Presentation/Root/RootHolder.swift index 9687e67b..d5532f96 100644 --- a/iosApp/iosApp/Presentation/Root/RootHolder.swift +++ b/iosApp/iosApp/Presentation/Root/RootHolder.swift @@ -16,12 +16,12 @@ final class RootHolder { init() { lifecycle = LifecycleRegistryKt.LifecycleRegistry() - let platformConfiguration = DefaultIosPlatformConfiguration() - ServicesModuleCompanion.shared.platformConfiguration.initialize(value: platformConfiguration) + let platformConfiguration = DefaultNativePlatformConfiguration() + RootModuleCompanion.shared.servicesModule.platformConfiguration.initialize(value: platformConfiguration) root = DefaultRootComponent( componentContext: DefaultComponentContext(lifecycle: lifecycle), rootModule: RootModuleCompanion.shared, - servicesModule: ServicesModuleCompanion.shared + servicesModule: RootModuleCompanion.shared.servicesModule ) lifecycle.onCreate() } diff --git a/iosApp/iosApp/Presentation/Root/RootView.swift b/iosApp/iosApp/Presentation/Root/RootView.swift index 4bd1cfc7..b8d3e860 100644 --- a/iosApp/iosApp/Presentation/Root/RootView.swift +++ b/iosApp/iosApp/Presentation/Root/RootView.swift @@ -45,7 +45,7 @@ private struct ChildView: View { case let child as DefaultRootComponentConfigurationSplash: SplashView(root, child.splashComponent) case let child as DefaultRootComponentConfigurationStatus: - StatusView(root, child.statusComponents) + StatusView(root, child.rootStatusComponent) default: EmptyView() } diff --git a/iosApp/iosApp/Presentation/Status/StatusView.swift b/iosApp/iosApp/Presentation/Status/StatusView.swift index e59b20d5..3cb81227 100644 --- a/iosApp/iosApp/Presentation/Status/StatusView.swift +++ b/iosApp/iosApp/Presentation/Status/StatusView.swift @@ -11,10 +11,10 @@ import Root struct StatusView: View { let rootComponent: RootComponent - let statusComponents: [StatusComponent] + let statusComponent: RootStatusComponent - init(_ rootComponent: RootComponent, _ statusComponents: [StatusComponent]) { - self.statusComponents = statusComponents + init(_ rootComponent: RootComponent, _ statusComponent: RootStatusComponent) { + self.statusComponent = statusComponent self.rootComponent = rootComponent } var body: some View { @@ -27,7 +27,7 @@ struct StatusView: View { Text(MR.strings.shared.status_subtitle.desc().localized()) .font(.body) - List(statusComponents,id: \.model.description) { statusComponent in + List(statusComponent.statusComponents,id: \.model.description) { statusComponent in StatusWidget(statusComponent) }.listStyle(.inset) } diff --git a/modules/features/root/build.gradle.kts b/modules/features/root/build.gradle.kts index 0994d47e..f9c556f1 100644 --- a/modules/features/root/build.gradle.kts +++ b/modules/features/root/build.gradle.kts @@ -28,6 +28,7 @@ buildConfig { kotlin { android() ios() + iosSimulatorArm64() cocoapods { summary = projectInfo.description homepage = projectInfo.url @@ -45,6 +46,7 @@ kotlin { export(libs.essenty) export(libs.moko.mvvm.core) export(libs.moko.mvvm.flow) + export(libs.klibs.mikro.platform) } } sourceSets { @@ -54,7 +56,7 @@ kotlin { implementation(libs.mppsettings) // klibs implementation(libs.klibs.mikro.core) - implementation(libs.klibs.mikro.platform) + api(libs.klibs.mikro.platform) implementation(libs.klibs.kstorage) implementation(libs.klibs.kdi) // Decompose @@ -99,6 +101,10 @@ kotlin { val iosArm64Main by getting { resources.srcDirs("build/generated/moko/iosArm64Main/src") } + val iosSimulatorArm64Main by getting { + this.dependsOn(iosMain) + resources.srcDirs("build/generated/moko/iosSimulatorArm64Main/src") + } } } android { diff --git a/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt b/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt index a4ec0579..240aea4f 100644 --- a/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt +++ b/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt @@ -1,5 +1,6 @@ package com.makeevrserg.empireprojekt.mobile.features.root.di.factories +import com.russhwolf.settings.NSUserDefaultsSettings import com.russhwolf.settings.Settings import ru.astrainteractive.klibs.kdi.Factory import ru.astrainteractive.klibs.mikro.platform.PlatformConfiguration @@ -9,6 +10,6 @@ actual class SettingsFactory actual constructor( private val configuration: PlatformConfiguration ) : Factory { override fun create(): Settings { - TODO() + return NSUserDefaultsSettings.Factory().create("SETTINGS") } } diff --git a/modules/features/splash/build.gradle.kts b/modules/features/splash/build.gradle.kts index 48552372..8f41523e 100644 --- a/modules/features/splash/build.gradle.kts +++ b/modules/features/splash/build.gradle.kts @@ -12,6 +12,7 @@ plugins { kotlin { android() ios() + iosSimulatorArm64() sourceSets { val commonMain by getting { dependencies { diff --git a/modules/services/core/build.gradle.kts b/modules/services/core/build.gradle.kts index a8fe4fb0..de87fa5d 100644 --- a/modules/services/core/build.gradle.kts +++ b/modules/services/core/build.gradle.kts @@ -12,6 +12,7 @@ plugins { kotlin { android() ios() + iosSimulatorArm64() sourceSets { val commonMain by getting { dependencies { diff --git a/modules/services/resources/build.gradle.kts b/modules/services/resources/build.gradle.kts index 432fc6cd..4b375126 100644 --- a/modules/services/resources/build.gradle.kts +++ b/modules/services/resources/build.gradle.kts @@ -10,6 +10,7 @@ plugins { kotlin { android() ios() + iosSimulatorArm64() sourceSets { val commonMain by getting { dependencies { @@ -22,6 +23,9 @@ kotlin { val iosArm64Main by getting { resources.srcDirs("build/generated/moko/iosArm64Main/src") } + val iosSimulatorArm64Main by getting { + resources.srcDirs("build/generated/moko/iosSimulatorArm64Main/src") + } } } multiplatformResources { @@ -32,4 +36,7 @@ android { dependencies { implementation("com.google.android.material:material:1.9.0") } + sourceSets { + getByName("main").java.srcDirs("build/generated/moko/androidMain/src") + } } \ No newline at end of file From b94fdd875bcacffb723a80491d8c82b1b55127f8 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Sun, 20 Aug 2023 14:01:00 +0300 Subject: [PATCH 11/20] fix: release build --- androidApp/build.gradle.kts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 76b91509..2300e71a 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -22,6 +22,7 @@ android { applicationId = projectInfo.group versionCode = gradleProperty("project.version.code").integer versionName = projectInfo.versionString + setProperty("archivesBaseName", "${projectInfo.name}-${projectInfo.versionString}") } defaultConfig { multiDexEnabled = true @@ -85,12 +86,8 @@ android { add("META-INF/LGPL2.1") } } - buildTypes { -// applicationVariants.all( -// com.makeevrserg.empireprojekt.mobile.ApplicationVariantAction( -// project -// ) -// ) + lint { + abortOnError = false } } From f7830d5c36c640dc4dfc745dfa861ee199cb25b7 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Wed, 11 Oct 2023 21:00:22 +0300 Subject: [PATCH 12/20] refactor: fix packages, class names --- .../empireprojekt/mobile/MainActivity.kt | 8 +- .../LinkBrowserFactory.kt | 2 +- .../{factories => factory}/SettingsFactory.kt | 2 +- .../features/root/DefaultRootComponent.kt | 85 +++---------------- .../mobile/features/root/RootComponent.kt | 24 +----- .../mobile/features/root/di/RootModule.kt | 4 +- .../LinkBrowserFactory.kt | 2 +- .../{factories => factory}/SettingsFactory.kt | 2 +- .../root/di/impl/root/RootModuleImpl.kt | 8 +- .../root/di/impl/root/ServicesModuleImpl.kt | 2 +- .../impl/splash/SplashComponentModuleImpl.kt | 3 +- .../DefaultRootBottomSheetComponent.kt | 2 +- .../{ => modal}/RootBottomSheetComponent.kt | 4 +- .../root/screen/DefaultRootScreenComponent.kt | 65 ++++++++++++++ .../root/screen/RootScreenComponent.kt | 24 ++++++ .../RootScreenComponentChildFactory.kt | 36 ++++++++ .../DefaultThemeSwitcherComponentComponent.kt | 42 +++++++++ .../features/theme/PreviewThemeSwitcher.kt | 25 ------ .../theme/PreviewThemeSwitcherComponent.kt | 25 ++++++ .../mobile/features/theme/ThemeSwitcher.kt | 16 ---- .../features/theme/ThemeSwitcherComponent.kt | 42 ++------- .../LinkBrowserFactory.kt | 2 +- .../{factories => factory}/SettingsFactory.kt | 2 +- ...onentImpl.kt => DefaultSplashComponent.kt} | 2 +- .../splash/data/SplashComponentRepository.kt | 9 -- .../data/SplashComponentRepositoryImpl.kt | 7 ++ .../logic/splash/SplashComponentTest.kt | 4 +- .../features/ui/root/ApplicationContent.kt | 16 ++-- .../features/ui/root/ComposeApplication.kt | 17 ++-- .../mobile/features/ui/splash/SplashScreen.kt | 6 +- .../mobile/features/ui/status/StatusScreen.kt | 20 ++--- .../mobile/{utils => util}/ComposeUtils.kt | 2 +- .../{utils => util}/SharedBackHandler.kt | 2 +- .../empireprojekt/mobile/wear/MainActivity.kt | 2 +- .../mobile/wear/di/WearRootModule.kt | 4 +- .../mobile/wear/di/impl/WearRootModuleImpl.kt | 8 +- .../mobile/wear/features/main/MainScreen.kt | 4 +- .../features/main/components/ThemeChip.kt | 16 ++-- 38 files changed, 294 insertions(+), 252 deletions(-) rename modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/{factories => factory}/LinkBrowserFactory.kt (98%) rename modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/{factories => factory}/SettingsFactory.kt (98%) rename modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/{factories => factory}/LinkBrowserFactory.kt (97%) rename modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/{factories => factory}/SettingsFactory.kt (97%) rename modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/{ => modal}/DefaultRootBottomSheetComponent.kt (95%) rename modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/{ => modal}/RootBottomSheetComponent.kt (81%) create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/DefaultRootScreenComponent.kt create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/RootScreenComponent.kt create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/di/factory/RootScreenComponentChildFactory.kt create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt delete mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcher.kt create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcherComponent.kt delete mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcher.kt rename modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/{factories => factory}/LinkBrowserFactory.kt (98%) rename modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/{factories => factory}/SettingsFactory.kt (98%) rename modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/{SplashComponentImpl.kt => DefaultSplashComponent.kt} (96%) create mode 100644 modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/data/SplashComponentRepositoryImpl.kt rename modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/{utils => util}/ComposeUtils.kt (88%) rename modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/{utils => util}/SharedBackHandler.kt (90%) diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/MainActivity.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/MainActivity.kt index 4630191e..b22fed90 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/MainActivity.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/MainActivity.kt @@ -8,16 +8,15 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.runtime.getValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat import com.arkivanov.decompose.defaultComponentContext import com.makeevrserg.empireprojekt.mobile.core.ui.rememberSlotModalBottomSheetState import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme -import com.makeevrserg.empireprojekt.mobile.features.root.DefaultRootBottomSheetComponent import com.makeevrserg.empireprojekt.mobile.features.root.DefaultRootComponent import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule +import com.makeevrserg.empireprojekt.mobile.features.root.modal.DefaultRootBottomSheetComponent import com.makeevrserg.empireprojekt.mobile.features.ui.info.InfoScreen import com.makeevrserg.empireprojekt.mobile.features.ui.root.ApplicationContent import com.makeevrserg.empireprojekt.mobile.features.ui.root.ComposeApplication @@ -37,7 +36,7 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setTheme(R.style.AppTheme) val componentContext = defaultComponentContext() - val rootComponent = DefaultRootComponent(componentContext, rootModule, servicesModule) + val rootComponent = DefaultRootComponent(componentContext, rootModule) val rootBottomSheetComponent = rootComponent.rootBottomSheetComponent setContent { @@ -51,7 +50,7 @@ class MainActivity : ComponentActivity() { } } } - ComposeApplication(rootModule.themeSwitcher.value) { + ComposeApplication(rootModule.themeSwitcherComponent.value) { ModalBottomSheetLayout( sheetState = bottomSheetState.sheetState, sheetContent = bottomSheetState.sheetContent.value, @@ -60,7 +59,6 @@ class MainActivity : ComponentActivity() { ) { ApplicationContent( rootComponent = rootComponent, - rootBottomSheetComponent = rootBottomSheetComponent, modifier = Modifier ) } diff --git a/modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/LinkBrowserFactory.kt b/modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/LinkBrowserFactory.kt similarity index 98% rename from modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/LinkBrowserFactory.kt rename to modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/LinkBrowserFactory.kt index 73466728..497a1536 100644 --- a/modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/LinkBrowserFactory.kt +++ b/modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/LinkBrowserFactory.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.features.root.di.factories +package com.makeevrserg.empireprojekt.mobile.features.root.di.factory import com.makeevrserg.empireprojekt.mobile.services.core.AndroidLinkBrowser import com.makeevrserg.empireprojekt.mobile.services.core.LinkBrowser diff --git a/modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt b/modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/SettingsFactory.kt similarity index 98% rename from modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt rename to modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/SettingsFactory.kt index 8cfbb9dd..c387bc1b 100644 --- a/modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt +++ b/modules/features/root/src/androidMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/SettingsFactory.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.features.root.di.factories +package com.makeevrserg.empireprojekt.mobile.features.root.di.factory import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt index 6853ac33..07ec88db 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootComponent.kt @@ -2,85 +2,20 @@ package com.makeevrserg.empireprojekt.mobile.features.root import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.childContext -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.pop -import com.arkivanov.decompose.router.stack.push -import com.arkivanov.decompose.router.stack.replaceAll -import com.arkivanov.decompose.router.stack.replaceCurrent -import com.arkivanov.decompose.value.Value -import com.makeevrserg.empireprojekt.mobile.features.logic.splash.SplashComponent -import com.makeevrserg.empireprojekt.mobile.features.logic.splash.SplashComponentImpl import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule -import com.makeevrserg.empireprojekt.mobile.features.root.di.ServicesModule -import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.splash.SplashComponentModuleImpl -import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent -import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.features.root.modal.DefaultRootBottomSheetComponent +import com.makeevrserg.empireprojekt.mobile.features.root.screen.DefaultRootScreenComponent class DefaultRootComponent( componentContext: ComponentContext, - rootModule: RootModule, - servicesModule: ServicesModule + rootModule: RootModule ) : RootComponent, ComponentContext by componentContext { - override val rootBottomSheetComponent: RootBottomSheetComponent = - DefaultRootBottomSheetComponent( - componentContext = childContext("RootBottomSheetComponent"), - servicesModule = servicesModule, - ) - private val navigation = StackNavigation() - - override val childStack: Value> = childStack( - source = navigation, - initialConfiguration = RootComponent.Child.Splash, - handleBackButton = true, - childFactory = { config, context -> - when (config) { - RootComponent.Child.Splash -> Configuration.Splash( - splashComponent = SplashComponentImpl( - context = context, - module = SplashComponentModuleImpl( - rootModule = rootModule, - servicesModule = servicesModule - ) - ) - ) - - RootComponent.Child.Status -> { - Configuration.Status( - themeSwitcher = rootModule.themeSwitcher.value, - rootStatusComponent = rootModule.rootStatusComponent.value - ) - } - } - } + override val rootBottomSheetComponent = DefaultRootBottomSheetComponent( + componentContext = childContext("RootBottomSheetComponent"), + servicesModule = rootModule.servicesModule, + ) + override val rootScreenComponent = DefaultRootScreenComponent( + componentContext = childContext("RootScreenComponent"), + rootModule = rootModule, ) - - override fun push(screen: RootComponent.Child) { - navigation.push(screen) - } - - override fun replaceCurrent(screen: RootComponent.Child) { - navigation.replaceCurrent(screen) - } - - override fun replaceAll(screen: RootComponent.Child) { - navigation.replaceAll(screen) - } - - override fun pop() { - navigation.pop() - } - - sealed interface Configuration { - - class Splash( - val splashComponent: SplashComponent - ) : Configuration - - class Status( - val rootStatusComponent: RootStatusComponent, - val themeSwitcher: ThemeSwitcher - ) : Configuration - } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/RootComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/RootComponent.kt index d1bdb149..28a304f3 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/RootComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/RootComponent.kt @@ -1,25 +1,9 @@ package com.makeevrserg.empireprojekt.mobile.features.root -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.value.Value -import com.arkivanov.essenty.backhandler.BackHandlerOwner -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize +import com.makeevrserg.empireprojekt.mobile.features.root.modal.RootBottomSheetComponent +import com.makeevrserg.empireprojekt.mobile.features.root.screen.RootScreenComponent -interface RootComponent : BackHandlerOwner { +interface RootComponent { + val rootScreenComponent: RootScreenComponent val rootBottomSheetComponent: RootBottomSheetComponent - val childStack: Value> - - fun push(screen: Child) - fun replaceCurrent(screen: Child) - fun replaceAll(screen: Child) - fun pop() - - sealed interface Child : Parcelable { - @Parcelize - object Splash : Child - - @Parcelize - object Status : Child - } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt index ebfafa52..1738d9cd 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt @@ -2,7 +2,7 @@ package com.makeevrserg.empireprojekt.mobile.features.root.di import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.root.RootModuleImpl import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent -import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent import com.russhwolf.settings.Settings import kotlinx.coroutines.CoroutineScope import ru.astrainteractive.klibs.kdi.Module @@ -15,7 +15,7 @@ interface RootModule : Module { val settings: Single val dispatchers: Single val mainScope: Single - val themeSwitcher: Single + val themeSwitcherComponent: Single val rootStatusComponent: Single companion object : RootModule by RootModuleImpl } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/LinkBrowserFactory.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/LinkBrowserFactory.kt similarity index 97% rename from modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/LinkBrowserFactory.kt rename to modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/LinkBrowserFactory.kt index be66437e..83fe86ca 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/LinkBrowserFactory.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/LinkBrowserFactory.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.features.root.di.factories +package com.makeevrserg.empireprojekt.mobile.features.root.di.factory import com.makeevrserg.empireprojekt.mobile.services.core.LinkBrowser import ru.astrainteractive.klibs.kdi.Factory diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/SettingsFactory.kt similarity index 97% rename from modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt rename to modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/SettingsFactory.kt index f933ce85..da0ece6a 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/SettingsFactory.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.features.root.di.factories +package com.makeevrserg.empireprojekt.mobile.features.root.di.factory import com.russhwolf.settings.Settings import ru.astrainteractive.klibs.kdi.Factory diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt index 0197e08d..c172f96c 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt @@ -1,11 +1,11 @@ package com.makeevrserg.empireprojekt.mobile.features.root.di.impl.root import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule -import com.makeevrserg.empireprojekt.mobile.features.root.di.factories.SettingsFactory +import com.makeevrserg.empireprojekt.mobile.features.root.di.factory.SettingsFactory import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.status.StatusModuleImpl import com.makeevrserg.empireprojekt.mobile.features.status.root.DefaultRootStatusComponent import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent -import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.features.theme.DefaultThemeSwitcherComponentComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent import kotlinx.coroutines.MainScope import ru.astrainteractive.klibs.kdi.Single @@ -30,8 +30,8 @@ internal object RootModuleImpl : RootModule { override val mainScope = Single { MainScope() } - override val themeSwitcher: Single = Single { - ThemeSwitcherComponent(settings.value) + override val themeSwitcherComponent: Single = Single { + DefaultThemeSwitcherComponentComponent(settings.value) } override val rootStatusComponent: Single = Single { DefaultRootStatusComponent(StatusModuleImpl(this)) diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/ServicesModuleImpl.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/ServicesModuleImpl.kt index 91961cd9..a81b897d 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/ServicesModuleImpl.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/ServicesModuleImpl.kt @@ -1,7 +1,7 @@ package com.makeevrserg.empireprojekt.mobile.features.root.di.impl.root import com.makeevrserg.empireprojekt.mobile.features.root.di.ServicesModule -import com.makeevrserg.empireprojekt.mobile.features.root.di.factories.LinkBrowserFactory +import com.makeevrserg.empireprojekt.mobile.features.root.di.factory.LinkBrowserFactory import com.makeevrserg.empireprojekt.mobile.services.core.LinkBrowser import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/splash/SplashComponentModuleImpl.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/splash/SplashComponentModuleImpl.kt index 60535685..39df9de4 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/splash/SplashComponentModuleImpl.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/splash/SplashComponentModuleImpl.kt @@ -1,6 +1,7 @@ package com.makeevrserg.empireprojekt.mobile.features.root.di.impl.splash import com.makeevrserg.empireprojekt.mobile.features.logic.splash.data.SplashComponentRepository +import com.makeevrserg.empireprojekt.mobile.features.logic.splash.data.SplashComponentRepositoryImpl import com.makeevrserg.empireprojekt.mobile.features.logic.splash.di.SplashComponentModule import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule import com.makeevrserg.empireprojekt.mobile.features.root.di.ServicesModule @@ -18,6 +19,6 @@ class SplashComponentModuleImpl( override val scope: CoroutineScope by rootModule.mainScope override val dispatchers: KotlinDispatchers by rootModule.dispatchers override val repository: SplashComponentRepository = Provider { - SplashComponentRepository.Default() + SplashComponentRepositoryImpl() }.provide() } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootBottomSheetComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/modal/DefaultRootBottomSheetComponent.kt similarity index 95% rename from modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootBottomSheetComponent.kt rename to modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/modal/DefaultRootBottomSheetComponent.kt index 813f2c18..60038c3e 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/DefaultRootBottomSheetComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/modal/DefaultRootBottomSheetComponent.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.features.root +package com.makeevrserg.empireprojekt.mobile.features.root.modal import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.slot.ChildSlot diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/RootBottomSheetComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/modal/RootBottomSheetComponent.kt similarity index 81% rename from modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/RootBottomSheetComponent.kt rename to modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/modal/RootBottomSheetComponent.kt index 59380d83..98f8012f 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/RootBottomSheetComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/modal/RootBottomSheetComponent.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.features.root +package com.makeevrserg.empireprojekt.mobile.features.root.modal import com.arkivanov.decompose.router.slot.ChildSlot import com.arkivanov.decompose.value.Value @@ -12,6 +12,6 @@ interface RootBottomSheetComponent { sealed interface Child : Parcelable { @Parcelize - object Settings : Child + data object Settings : Child } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/DefaultRootScreenComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/DefaultRootScreenComponent.kt new file mode 100644 index 00000000..213b9221 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/DefaultRootScreenComponent.kt @@ -0,0 +1,65 @@ +package com.makeevrserg.empireprojekt.mobile.features.root.screen + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import com.arkivanov.decompose.router.stack.replaceAll +import com.arkivanov.decompose.router.stack.replaceCurrent +import com.arkivanov.decompose.value.Value +import com.makeevrserg.empireprojekt.mobile.features.logic.splash.SplashComponent +import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule +import com.makeevrserg.empireprojekt.mobile.features.root.screen.di.factory.RootScreenComponentChildFactory +import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent + +class DefaultRootScreenComponent( + componentContext: ComponentContext, + rootModule: RootModule, +) : RootScreenComponent, ComponentContext by componentContext { + + private val navigation = StackNavigation() + + override val childStack: Value> = childStack( + source = navigation, + initialConfiguration = RootScreenComponent.Child.Splash, + handleBackButton = true, + childFactory = { config, context -> + RootScreenComponentChildFactory( + config = config, + context = context, + rootModule = rootModule + ).create() + } + ) + + override fun push(screen: RootScreenComponent.Child) { + navigation.push(screen) + } + + override fun replaceCurrent(screen: RootScreenComponent.Child) { + navigation.replaceCurrent(screen) + } + + override fun replaceAll(screen: RootScreenComponent.Child) { + navigation.replaceAll(screen) + } + + override fun pop() { + navigation.pop() + } + + sealed interface Configuration { + + class Splash( + val splashComponent: SplashComponent + ) : Configuration + + class Status( + val rootStatusComponent: RootStatusComponent, + val themeSwitcherComponent: ThemeSwitcherComponent + ) : Configuration + } +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/RootScreenComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/RootScreenComponent.kt new file mode 100644 index 00000000..bd6c5fb4 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/RootScreenComponent.kt @@ -0,0 +1,24 @@ +package com.makeevrserg.empireprojekt.mobile.features.root.screen + +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.value.Value +import com.arkivanov.essenty.backhandler.BackHandlerOwner +import com.arkivanov.essenty.parcelable.Parcelable +import com.arkivanov.essenty.parcelable.Parcelize + +interface RootScreenComponent : BackHandlerOwner { + val childStack: Value> + + fun push(screen: Child) + fun replaceCurrent(screen: Child) + fun replaceAll(screen: Child) + fun pop() + + sealed interface Child : Parcelable { + @Parcelize + object Splash : Child + + @Parcelize + object Status : Child + } +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/di/factory/RootScreenComponentChildFactory.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/di/factory/RootScreenComponentChildFactory.kt new file mode 100644 index 00000000..af744d33 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/di/factory/RootScreenComponentChildFactory.kt @@ -0,0 +1,36 @@ +package com.makeevrserg.empireprojekt.mobile.features.root.screen.di.factory + +import com.arkivanov.decompose.ComponentContext +import com.makeevrserg.empireprojekt.mobile.features.logic.splash.DefaultSplashComponent +import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule +import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.splash.SplashComponentModuleImpl +import com.makeevrserg.empireprojekt.mobile.features.root.screen.DefaultRootScreenComponent +import com.makeevrserg.empireprojekt.mobile.features.root.screen.RootScreenComponent +import ru.astrainteractive.klibs.kdi.Factory + +class RootScreenComponentChildFactory( + private val config: RootScreenComponent.Child, + private val context: ComponentContext, + private val rootModule: RootModule +) : Factory { + override fun create(): DefaultRootScreenComponent.Configuration { + return when (config) { + RootScreenComponent.Child.Splash -> DefaultRootScreenComponent.Configuration.Splash( + splashComponent = DefaultSplashComponent( + context = context, + module = SplashComponentModuleImpl( + rootModule = rootModule, + servicesModule = rootModule.servicesModule + ) + ) + ) + + RootScreenComponent.Child.Status -> { + DefaultRootScreenComponent.Configuration.Status( + themeSwitcherComponent = rootModule.themeSwitcherComponent.value, + rootStatusComponent = rootModule.rootStatusComponent.value + ) + } + } + } +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt new file mode 100644 index 00000000..84722115 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt @@ -0,0 +1,42 @@ +package com.makeevrserg.empireprojekt.mobile.features.theme + +import com.russhwolf.settings.Settings +import kotlinx.coroutines.flow.StateFlow +import ru.astrainteractive.klibs.kstorage.StateFlowMutableStorageValue +import ru.astrainteractive.klibs.mikro.core.util.next + +class DefaultThemeSwitcherComponentComponent(private val settings: Settings) : ThemeSwitcherComponent { + private val key = "THEME" + private val default = ThemeSwitcherComponent.Theme.DARK + private val themeFlowStorageValue = StateFlowMutableStorageValue( + default = default, + loadSettingsValue = { + val ordinal = settings.getInt(key, ThemeSwitcherComponent.Theme.LIGHT.ordinal) + ThemeSwitcherComponent.Theme.values().getOrNull(ordinal) ?: default + }, + saveSettingsValue = { + settings.putInt(key, it.ordinal) + } + ) + override val theme: StateFlow = themeFlowStorageValue.stateFlow + + override fun selectDarkTheme() { + themeFlowStorageValue.save(ThemeSwitcherComponent.Theme.DARK) + } + + override fun selectLightTheme() { + themeFlowStorageValue.save(ThemeSwitcherComponent.Theme.LIGHT) + } + + override fun selectTheme(theme: ThemeSwitcherComponent.Theme) { + themeFlowStorageValue.save(theme) + } + + override fun next() { + selectTheme(theme.value.next(ThemeSwitcherComponent.Theme.values())) + } + + init { + themeFlowStorageValue.load() + } +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcher.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcher.kt deleted file mode 100644 index 69ed4228..00000000 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcher.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.makeevrserg.empireprojekt.mobile.features.theme - -import kotlinx.coroutines.flow.MutableStateFlow -import ru.astrainteractive.klibs.mikro.core.util.next - -class PreviewThemeSwitcher : ThemeSwitcher { - override val theme: MutableStateFlow = - MutableStateFlow(ThemeSwitcher.Theme.LIGHT) - - override fun selectDarkTheme() { - selectTheme(ThemeSwitcher.Theme.DARK) - } - - override fun selectLightTheme() { - selectTheme(ThemeSwitcher.Theme.LIGHT) - } - - override fun selectTheme(theme: ThemeSwitcher.Theme) { - this.theme.value = theme - } - - override fun next() { - theme.value.next(ThemeSwitcher.Theme.values()).run(::selectTheme) - } -} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcherComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcherComponent.kt new file mode 100644 index 00000000..7a15d321 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcherComponent.kt @@ -0,0 +1,25 @@ +package com.makeevrserg.empireprojekt.mobile.features.theme + +import kotlinx.coroutines.flow.MutableStateFlow +import ru.astrainteractive.klibs.mikro.core.util.next + +class PreviewThemeSwitcherComponent : ThemeSwitcherComponent { + override val theme: MutableStateFlow = + MutableStateFlow(ThemeSwitcherComponent.Theme.LIGHT) + + override fun selectDarkTheme() { + selectTheme(ThemeSwitcherComponent.Theme.DARK) + } + + override fun selectLightTheme() { + selectTheme(ThemeSwitcherComponent.Theme.LIGHT) + } + + override fun selectTheme(theme: ThemeSwitcherComponent.Theme) { + this.theme.value = theme + } + + override fun next() { + theme.value.next(ThemeSwitcherComponent.Theme.values()).run(::selectTheme) + } +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcher.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcher.kt deleted file mode 100644 index 4cd49c17..00000000 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcher.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.makeevrserg.empireprojekt.mobile.features.theme - -import kotlinx.coroutines.flow.StateFlow - -interface ThemeSwitcher { - val theme: StateFlow - - enum class Theme { - DARK, LIGHT - } - - fun selectDarkTheme() - fun selectLightTheme() - fun selectTheme(theme: Theme) - fun next() -} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt index 9fa9e3d1..4bb7e468 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt @@ -1,42 +1,16 @@ package com.makeevrserg.empireprojekt.mobile.features.theme -import com.russhwolf.settings.Settings import kotlinx.coroutines.flow.StateFlow -import ru.astrainteractive.klibs.kstorage.StateFlowMutableStorageValue -import ru.astrainteractive.klibs.mikro.core.util.next -class ThemeSwitcherComponent(private val settings: Settings) : ThemeSwitcher { - private val key = "THEME" - private val default = ThemeSwitcher.Theme.DARK - private val themeFlowStorageValue = StateFlowMutableStorageValue( - default = default, - loadSettingsValue = { - val ordinal = settings.getInt(key, ThemeSwitcher.Theme.LIGHT.ordinal) - ThemeSwitcher.Theme.values().getOrNull(ordinal) ?: default - }, - saveSettingsValue = { - settings.putInt(key, it.ordinal) - } - ) - override val theme: StateFlow = themeFlowStorageValue.stateFlow +interface ThemeSwitcherComponent { + val theme: StateFlow - override fun selectDarkTheme() { - themeFlowStorageValue.save(ThemeSwitcher.Theme.DARK) + enum class Theme { + DARK, LIGHT } - override fun selectLightTheme() { - themeFlowStorageValue.save(ThemeSwitcher.Theme.LIGHT) - } - - override fun selectTheme(theme: ThemeSwitcher.Theme) { - themeFlowStorageValue.save(theme) - } - - override fun next() { - selectTheme(theme.value.next(ThemeSwitcher.Theme.values())) - } - - init { - themeFlowStorageValue.load() - } + fun selectDarkTheme() + fun selectLightTheme() + fun selectTheme(theme: Theme) + fun next() } diff --git a/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/LinkBrowserFactory.kt b/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/LinkBrowserFactory.kt similarity index 98% rename from modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/LinkBrowserFactory.kt rename to modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/LinkBrowserFactory.kt index be41b2da..1dcab638 100644 --- a/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/LinkBrowserFactory.kt +++ b/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/LinkBrowserFactory.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.features.root.di.factories +package com.makeevrserg.empireprojekt.mobile.features.root.di.factory import com.makeevrserg.empireprojekt.mobile.services.core.LinkBrowser import ru.astrainteractive.klibs.kdi.Factory diff --git a/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt b/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/SettingsFactory.kt similarity index 98% rename from modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt rename to modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/SettingsFactory.kt index 240aea4f..21370679 100644 --- a/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factories/SettingsFactory.kt +++ b/modules/features/root/src/iosMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/factory/SettingsFactory.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.features.root.di.factories +package com.makeevrserg.empireprojekt.mobile.features.root.di.factory import com.russhwolf.settings.NSUserDefaultsSettings import com.russhwolf.settings.Settings diff --git a/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/SplashComponentImpl.kt b/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/DefaultSplashComponent.kt similarity index 96% rename from modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/SplashComponentImpl.kt rename to modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/DefaultSplashComponent.kt index 3a62bc3f..ff240c06 100644 --- a/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/SplashComponentImpl.kt +++ b/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/DefaultSplashComponent.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.launch -class SplashComponentImpl( +class DefaultSplashComponent( context: ComponentContext, module: SplashComponentModule ) : SplashComponent, diff --git a/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/data/SplashComponentRepository.kt b/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/data/SplashComponentRepository.kt index 1baba25c..472ff68f 100644 --- a/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/data/SplashComponentRepository.kt +++ b/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/data/SplashComponentRepository.kt @@ -2,13 +2,4 @@ package com.makeevrserg.empireprojekt.mobile.features.logic.splash.data interface SplashComponentRepository { fun isInitialLaunch(): Boolean - - /** - * Default implementation for this interface from [LocalPreferenceSource] - */ - class Default : SplashComponentRepository { - override fun isInitialLaunch(): Boolean { - return true - } - } } diff --git a/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/data/SplashComponentRepositoryImpl.kt b/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/data/SplashComponentRepositoryImpl.kt new file mode 100644 index 00000000..0e09947b --- /dev/null +++ b/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/data/SplashComponentRepositoryImpl.kt @@ -0,0 +1,7 @@ +package com.makeevrserg.empireprojekt.mobile.features.logic.splash.data + +class SplashComponentRepositoryImpl : SplashComponentRepository { + override fun isInitialLaunch(): Boolean { + return true + } +} diff --git a/modules/features/splash/src/commonTest/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/SplashComponentTest.kt b/modules/features/splash/src/commonTest/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/SplashComponentTest.kt index 8c9d4e9b..9bdbe979 100644 --- a/modules/features/splash/src/commonTest/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/SplashComponentTest.kt +++ b/modules/features/splash/src/commonTest/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/SplashComponentTest.kt @@ -29,7 +29,7 @@ class SplashComponentTest { fun TEST_initial_launch_true(): Unit = runBlocking { val expectInitialLaunchValue = true val splashComponent = - SplashComponentImpl(componentContext, buildModule(expectInitialLaunchValue)) + DefaultSplashComponent(componentContext, buildModule(expectInitialLaunchValue)) splashComponent.screenChannelFlow.test { val item = awaitItem() assertTrue(item is SplashComponent.Label.InitialLaunch) @@ -41,7 +41,7 @@ class SplashComponentTest { fun TEST_initial_launch_false(): Unit = runBlocking { val expectInitialLaunchValue = false val splashComponent = - SplashComponentImpl(componentContext, buildModule(expectInitialLaunchValue)) + DefaultSplashComponent(componentContext, buildModule(expectInitialLaunchValue)) splashComponent.screenChannelFlow.test { val item = awaitItem() assertTrue(item is SplashComponent.Label.InitialLaunch) diff --git a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ApplicationContent.kt b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ApplicationContent.kt index 87709fe4..b7aa4b4d 100644 --- a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ApplicationContent.kt +++ b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ApplicationContent.kt @@ -8,18 +8,17 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.stack.Children import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.slide import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.stackAnimation import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState -import com.makeevrserg.empireprojekt.mobile.features.root.DefaultRootComponent -import com.makeevrserg.empireprojekt.mobile.features.root.RootBottomSheetComponent +import com.makeevrserg.empireprojekt.mobile.features.root.RootComponent +import com.makeevrserg.empireprojekt.mobile.features.root.screen.DefaultRootScreenComponent import com.makeevrserg.empireprojekt.mobile.features.ui.splash.SplashScreenComponent import com.makeevrserg.empireprojekt.mobile.features.ui.status.StatusScreen @Composable fun ApplicationContent( - rootComponent: DefaultRootComponent, - rootBottomSheetComponent: RootBottomSheetComponent, + rootComponent: RootComponent, modifier: Modifier = Modifier ) { - val childStack by rootComponent.childStack.subscribeAsState() + val childStack by rootComponent.rootScreenComponent.childStack.subscribeAsState() Children( stack = childStack, modifier = modifier.fillMaxSize(), @@ -27,15 +26,14 @@ fun ApplicationContent( ) { configuration -> when (val screen = configuration.instance) { - is DefaultRootComponent.Configuration.Splash -> SplashScreenComponent( + is DefaultRootScreenComponent.Configuration.Splash -> SplashScreenComponent( rootComponent = rootComponent, splashComponent = screen.splashComponent ) - is DefaultRootComponent.Configuration.Status -> StatusScreen( + is DefaultRootScreenComponent.Configuration.Status -> StatusScreen( rootComponent = rootComponent, - rootBottomSheetComponent = rootBottomSheetComponent, - themeSwitcher = screen.themeSwitcher, + themeSwitcherComponent = screen.themeSwitcherComponent, rootStatusComponent = screen.rootStatusComponent ) } diff --git a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt index ba620871..70d1d3ac 100644 --- a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt +++ b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt @@ -8,17 +8,20 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme import com.makeevrserg.empireprojekt.mobile.core.ui.theme.LocalAppTheme -import com.makeevrserg.empireprojekt.mobile.features.theme.PreviewThemeSwitcher -import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.features.theme.PreviewThemeSwitcherComponent +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent -fun ThemeSwitcher.Theme.toComposeTheme() = when (this) { - ThemeSwitcher.Theme.DARK -> AppTheme.DefaultDarkTheme - ThemeSwitcher.Theme.LIGHT -> AppTheme.DefaultLightTheme +fun ThemeSwitcherComponent.Theme.toComposeTheme() = when (this) { + ThemeSwitcherComponent.Theme.DARK -> AppTheme.DefaultDarkTheme + ThemeSwitcherComponent.Theme.LIGHT -> AppTheme.DefaultLightTheme } @Composable -fun ComposeApplication(themeSwitcher: ThemeSwitcher = PreviewThemeSwitcher(), content: @Composable () -> Unit) { - val theme by themeSwitcher.theme.collectAsState() +fun ComposeApplication( + themeSwitcherComponent: ThemeSwitcherComponent = PreviewThemeSwitcherComponent(), + content: @Composable () -> Unit +) { + val theme by themeSwitcherComponent.theme.collectAsState() val appTheme = theme.toComposeTheme() TransparentBars(appTheme.isDark) diff --git a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/splash/SplashScreen.kt b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/splash/SplashScreen.kt index df572811..0b53a831 100644 --- a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/splash/SplashScreen.kt +++ b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/splash/SplashScreen.kt @@ -16,21 +16,21 @@ import com.makeevrserg.empireprojekt.mobile.core.ui.asPainter import com.makeevrserg.empireprojekt.mobile.core.ui.components.navBarsPadding import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme import com.makeevrserg.empireprojekt.mobile.features.logic.splash.SplashComponent -import com.makeevrserg.empireprojekt.mobile.features.root.DefaultRootComponent import com.makeevrserg.empireprojekt.mobile.features.root.RootComponent +import com.makeevrserg.empireprojekt.mobile.features.root.screen.RootScreenComponent import com.makeevrserg.empireprojekt.mobile.resources.MR import kotlinx.coroutines.flow.collectLatest @Composable fun SplashScreenComponent( splashComponent: SplashComponent, - rootComponent: DefaultRootComponent + rootComponent: RootComponent ) { LaunchedEffect(key1 = Unit) { splashComponent.screenChannelFlow.collectLatest { when (it) { is SplashComponent.Label.InitialLaunch -> { - rootComponent.replaceCurrent(RootComponent.Child.Status) + rootComponent.rootScreenComponent.replaceCurrent(RootScreenComponent.Child.Status) } } } diff --git a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt index 17a4024a..70379aef 100644 --- a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt +++ b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt @@ -18,19 +18,18 @@ import androidx.compose.ui.draw.clip import com.makeevrserg.empireprojekt.mobile.core.ui.asComposableString import com.makeevrserg.empireprojekt.mobile.core.ui.components.navBarsPadding import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme -import com.makeevrserg.empireprojekt.mobile.features.root.DefaultRootComponent -import com.makeevrserg.empireprojekt.mobile.features.root.RootBottomSheetComponent +import com.makeevrserg.empireprojekt.mobile.features.root.RootComponent +import com.makeevrserg.empireprojekt.mobile.features.root.modal.RootBottomSheetComponent import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent -import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent import com.makeevrserg.empireprojekt.mobile.features.ui.status.widget.StatusWidget import com.makeevrserg.empireprojekt.mobile.resources.MR import ru.astrainteractive.klibs.mikro.core.util.next @Composable fun StatusScreen( - rootComponent: DefaultRootComponent, - rootBottomSheetComponent: RootBottomSheetComponent, - themeSwitcher: ThemeSwitcher, + rootComponent: RootComponent, + themeSwitcherComponent: ThemeSwitcherComponent, rootStatusComponent: RootStatusComponent, ) { Scaffold( @@ -40,7 +39,7 @@ fun StatusScreen( modifier = Modifier.navBarsPadding(), backgroundColor = AppTheme.materialColor.secondaryVariant, onClick = { - rootBottomSheetComponent.pushSlot(RootBottomSheetComponent.Child.Settings) + rootComponent.rootBottomSheetComponent.pushSlot(RootBottomSheetComponent.Child.Settings) }, ) { Icon( @@ -65,9 +64,10 @@ fun StatusScreen( modifier = Modifier .clip(CircleShape) .clickable { - val nextTheme = - themeSwitcher.theme.value.next(ThemeSwitcher.Theme.values()) - themeSwitcher.selectTheme(nextTheme) + val nextTheme = themeSwitcherComponent.theme.value.next( + ThemeSwitcherComponent.Theme.values() + ) + themeSwitcherComponent.selectTheme(nextTheme) } ) } diff --git a/modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/utils/ComposeUtils.kt b/modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/util/ComposeUtils.kt similarity index 88% rename from modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/utils/ComposeUtils.kt rename to modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/util/ComposeUtils.kt index 9141f2f0..b2707540 100644 --- a/modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/utils/ComposeUtils.kt +++ b/modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/util/ComposeUtils.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.utils +package com.makeevrserg.empireprojekt.mobile.util import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearOutSlowInEasing diff --git a/modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/utils/SharedBackHandler.kt b/modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/util/SharedBackHandler.kt similarity index 90% rename from modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/utils/SharedBackHandler.kt rename to modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/util/SharedBackHandler.kt index b234aebe..08088c79 100644 --- a/modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/utils/SharedBackHandler.kt +++ b/modules/services/core-ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/util/SharedBackHandler.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.utils +package com.makeevrserg.empireprojekt.mobile.util import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt index acddb54a..095e3a3e 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt @@ -20,7 +20,7 @@ class MainActivity : ComponentActivity() { setContent { val navController = rememberSwipeDismissableNavController() val navHostRootComponent = NavHostRootComponent(navController) - ComposeApplication(rootModule.themeSwitcher.value) { + ComposeApplication(rootModule.themeSwitcherComponent.value) { NavigationScreen(navHostRootComponent) } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt index 111d233e..65f45d9e 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt @@ -1,6 +1,6 @@ package com.makeevrserg.empireprojekt.mobile.wear.di -import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent import com.makeevrserg.empireprojekt.mobile.wear.di.impl.WearRootModuleImpl import com.makeevrserg.empireprojekt.mobile.wear.features.status.WearStatusComponent import com.russhwolf.settings.Settings @@ -12,7 +12,7 @@ import ru.astrainteractive.klibs.mikro.platform.PlatformConfiguration interface WearRootModule : Module { val platformConfiguration: Lateinit val settings: Single - val themeSwitcher: Single + val themeSwitcherComponent: Single val wearStatusComponent: Single companion object : WearRootModule by WearRootModuleImpl diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt index d4298384..6c02bb5c 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt @@ -1,7 +1,7 @@ package com.makeevrserg.empireprojekt.mobile.wear.di.impl -import com.makeevrserg.empireprojekt.mobile.features.root.di.factories.SettingsFactory -import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.features.root.di.factory.SettingsFactory +import com.makeevrserg.empireprojekt.mobile.features.theme.DefaultThemeSwitcherComponentComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule import com.makeevrserg.empireprojekt.mobile.wear.features.status.WearStatusComponent @@ -16,8 +16,8 @@ object WearRootModuleImpl : WearRootModule { val configuration by platformConfiguration SettingsFactory(configuration).create() } - override val themeSwitcher: Single = Single { - ThemeSwitcherComponent(settings.value) + override val themeSwitcherComponent: Single = Single { + DefaultThemeSwitcherComponentComponent(settings.value) } override val wearStatusComponent: Single = Single { WearStatusComponent.Stub() diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt index 8c651732..60336f3d 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt @@ -25,7 +25,7 @@ fun MainScreen( wearRootModule: WearRootModule, rootComponent: NavHostRootComponent ) { - val themeSwitcher by wearRootModule.themeSwitcher + val themeSwitcher by wearRootModule.themeSwitcherComponent Scaffold( modifier = Modifier.background(AppTheme.materialColor.primaryVariant), vignette = { @@ -42,7 +42,7 @@ fun MainScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.height(AppTheme.dimens.S)) - ThemeChip(themeSwitcher = themeSwitcher) + ThemeChip(themeSwitcherComponent = themeSwitcher) Spacer(modifier = Modifier.height(AppTheme.dimens.S)) NavChip( text = "Statuses", diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt index 6a931776..25b0cd85 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt @@ -17,20 +17,20 @@ import androidx.wear.compose.material.ChipDefaults import androidx.wear.compose.material.Icon import androidx.wear.compose.material.Text import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme -import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent import com.makeevrserg.empireprojekt.mobile.wear.features.components.AstraChip @Composable -fun ThemeChip(themeSwitcher: ThemeSwitcher) { - val theme by themeSwitcher.theme.collectAsState() +fun ThemeChip(themeSwitcherComponent: ThemeSwitcherComponent) { + val theme by themeSwitcherComponent.theme.collectAsState() val icon = when (theme) { - ThemeSwitcher.Theme.DARK -> Icons.Filled.Bedtime - ThemeSwitcher.Theme.LIGHT -> Icons.Filled.WbSunny + ThemeSwitcherComponent.Theme.DARK -> Icons.Filled.Bedtime + ThemeSwitcherComponent.Theme.LIGHT -> Icons.Filled.WbSunny } val color by animateColorAsState( targetValue = when (theme) { - ThemeSwitcher.Theme.DARK -> AppTheme.materialColor.onPrimary - ThemeSwitcher.Theme.LIGHT -> AppTheme.materialColor.onPrimary + ThemeSwitcherComponent.Theme.DARK -> AppTheme.materialColor.onPrimary + ThemeSwitcherComponent.Theme.LIGHT -> AppTheme.materialColor.onPrimary }, label = "LABEL" ) @@ -43,7 +43,7 @@ fun ThemeChip(themeSwitcher: ThemeSwitcher) { color = AppTheme.materialColor.onPrimary ) }, - onClick = themeSwitcher::next, + onClick = themeSwitcherComponent::next, icon = { Crossfade(targetState = icon, label = "LABEL") { Icon( From 1367a8eec6dda06cf47b47ebeb4b6931a33af1b0 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Wed, 11 Oct 2023 21:23:14 +0300 Subject: [PATCH 13/20] refactor: di --- .../empireprojekt/mobile/MainActivity.kt | 8 ++-- .../empireprojekt/mobile/application/App.kt | 26 +++++++----- .../mobile/work/CheckStatusWork.kt | 11 ++++- .../mobile/features/root/di/RootModule.kt | 16 ++++--- .../mobile/features/root/di/ServicesModule.kt | 7 ++++ .../features/root/di/impl/RootModuleImpl.kt | 42 +++++++++++++++++++ .../di/impl/{root => }/ServicesModuleImpl.kt | 20 ++++++++- .../root/di/impl/root/RootModuleImpl.kt | 39 ----------------- .../impl/splash/SplashComponentModuleImpl.kt | 24 ----------- .../root/di/impl/status/StatusModuleImpl.kt | 14 ------- .../RootScreenComponentChildFactory.kt | 6 +-- .../mobile/features/status/di/StatusModule.kt | 6 +++ .../DefaultThemeSwitcherComponentComponent.kt | 14 +++++-- .../logic/splash/DefaultSplashComponent.kt | 2 +- .../logic/splash/di/SplashComponentModule.kt | 14 ++++++- .../logic/splash/SplashComponentTest.kt | 2 +- 16 files changed, 138 insertions(+), 113 deletions(-) create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/RootModuleImpl.kt rename modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/{root => }/ServicesModuleImpl.kt (69%) delete mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt delete mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/splash/SplashComponentModuleImpl.kt delete mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/status/StatusModuleImpl.kt diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/MainActivity.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/MainActivity.kt index b22fed90..0e34161c 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/MainActivity.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/MainActivity.kt @@ -12,15 +12,16 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat import com.arkivanov.decompose.defaultComponentContext +import com.makeevrserg.empireprojekt.mobile.application.App.Companion.asEmpireApp import com.makeevrserg.empireprojekt.mobile.core.ui.rememberSlotModalBottomSheetState import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme import com.makeevrserg.empireprojekt.mobile.features.root.DefaultRootComponent -import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule import com.makeevrserg.empireprojekt.mobile.features.root.modal.DefaultRootBottomSheetComponent import com.makeevrserg.empireprojekt.mobile.features.ui.info.InfoScreen import com.makeevrserg.empireprojekt.mobile.features.ui.root.ApplicationContent import com.makeevrserg.empireprojekt.mobile.features.ui.root.ComposeApplication import com.makeevrserg.empireprojekt.mobile.resources.R +import ru.astrainteractive.klibs.kdi.Provider import ru.astrainteractive.klibs.kdi.getValue @ExperimentalMaterialApi @@ -28,8 +29,9 @@ import ru.astrainteractive.klibs.kdi.getValue @ExperimentalAnimationApi @ExperimentalFoundationApi class MainActivity : ComponentActivity() { - private val rootModule by RootModule - private val servicesModule by rootModule.servicesModule + private val rootModule by Provider { + application.asEmpireApp().rootModule + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt index c5c23c45..2ee4aeb1 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt @@ -1,6 +1,7 @@ package com.makeevrserg.empireprojekt.mobile.application import android.app.Application +import android.content.Context import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager @@ -8,8 +9,7 @@ import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.data.WearDataLayerRegistry import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.initialize -import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule -import com.makeevrserg.empireprojekt.mobile.services.core.CoroutineFeature +import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.RootModuleImpl import com.makeevrserg.empireprojekt.mobile.work.CheckStatusWork import kotlinx.coroutines.cancel import kotlinx.coroutines.delay @@ -21,12 +21,13 @@ import java.util.concurrent.TimeUnit @OptIn(ExperimentalHorologistApi::class) class App : Application() { - private val servicesModule by RootModule.servicesModule - private val coroutineFeature = CoroutineFeature.Default() + val rootModule by lazy { + RootModuleImpl() + } private val wearDataLayerRegistry by lazy { WearDataLayerRegistry.fromContext( application = applicationContext, - coroutineScope = coroutineFeature + coroutineScope = rootModule.servicesModule.mainScope.value ) } private val messageClient by lazy { @@ -35,17 +36,17 @@ class App : Application() { override fun onTerminate() { super.onTerminate() - coroutineFeature.cancel() + rootModule.servicesModule.mainScope.value.cancel() } override fun onCreate() { super.onCreate() Firebase.initialize(this) - servicesModule.platformConfiguration.initialize( + rootModule.servicesModule.platformConfiguration.initialize { DefaultAndroidPlatformConfiguration( applicationContext ) - ) + } scheduleWork() } @@ -61,15 +62,20 @@ class App : Application() { ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, statusWork ) - coroutineFeature.launch { + rootModule.servicesModule.mainScope.value.launch { while (isActive) { delay(5000L) CheckStatusWork.sendMessageOnWear( wearDataLayerRegistry = wearDataLayerRegistry, - rootModule = RootModule, + rootModule = rootModule, messageClient = messageClient ) } } } + + companion object { + fun Application.asEmpireApp(): App = (this as App) + fun Context.asEmpireApp(): App = (applicationContext as App) + } } diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt index 96bb22c3..88b1ba76 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt @@ -7,12 +7,14 @@ import androidx.work.WorkerParameters import com.google.android.gms.wearable.MessageClient import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.data.WearDataLayerRegistry +import com.makeevrserg.empireprojekt.mobile.application.App.Companion.asEmpireApp import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.tasks.await +import ru.astrainteractive.klibs.kdi.Provider import ru.astrainteractive.klibs.kdi.getValue @OptIn(ExperimentalHorologistApi::class) @@ -20,8 +22,13 @@ class CheckStatusWork( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { - private val rootModule by RootModule - private val rootStatusComponent by rootModule.rootStatusComponent + + private val rootModule by lazy { + applicationContext.asEmpireApp().rootModule + } + private val rootStatusComponent by Provider { + rootModule.rootStatusComponent.value + } override suspend fun doWork(): Result = coroutineScope { Log.d(TAG, "doWork: ") diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt index 1738d9cd..e63ff206 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/RootModule.kt @@ -1,21 +1,19 @@ package com.makeevrserg.empireprojekt.mobile.features.root.di -import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.root.RootModuleImpl +import com.makeevrserg.empireprojekt.mobile.features.logic.splash.di.SplashComponentModule +import com.makeevrserg.empireprojekt.mobile.features.status.di.StatusModule import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent -import com.russhwolf.settings.Settings -import kotlinx.coroutines.CoroutineScope import ru.astrainteractive.klibs.kdi.Module import ru.astrainteractive.klibs.kdi.Single -import ru.astrainteractive.klibs.mikro.core.dispatchers.KotlinDispatchers interface RootModule : Module { + val servicesModule: ServicesModule + val statusModule: StatusModule + val splashModule: SplashComponentModule - val settings: Single - val dispatchers: Single - val mainScope: Single - val themeSwitcherComponent: Single + // Global components val rootStatusComponent: Single - companion object : RootModule by RootModuleImpl + val themeSwitcherComponent: Single } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/ServicesModule.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/ServicesModule.kt index d76a6248..1051df34 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/ServicesModule.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/ServicesModule.kt @@ -1,16 +1,23 @@ package com.makeevrserg.empireprojekt.mobile.features.root.di import com.makeevrserg.empireprojekt.mobile.services.core.LinkBrowser +import com.russhwolf.settings.Settings import io.ktor.client.HttpClient +import kotlinx.coroutines.CoroutineScope import kotlinx.serialization.json.Json import ru.astrainteractive.klibs.kdi.Lateinit import ru.astrainteractive.klibs.kdi.Module import ru.astrainteractive.klibs.kdi.Single +import ru.astrainteractive.klibs.mikro.core.dispatchers.KotlinDispatchers import ru.astrainteractive.klibs.mikro.platform.PlatformConfiguration interface ServicesModule : Module { + val platformConfiguration: Lateinit val jsonConfiguration: Single val httpClient: Single val linkBrowser: Single + val settings: Single + val dispatchers: Single + val mainScope: Single } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/RootModuleImpl.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/RootModuleImpl.kt new file mode 100644 index 00000000..219cac49 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/RootModuleImpl.kt @@ -0,0 +1,42 @@ +package com.makeevrserg.empireprojekt.mobile.features.root.di.impl + +import com.makeevrserg.empireprojekt.mobile.features.logic.splash.di.SplashComponentModule +import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule +import com.makeevrserg.empireprojekt.mobile.features.root.di.ServicesModule +import com.makeevrserg.empireprojekt.mobile.features.status.di.StatusModule +import com.makeevrserg.empireprojekt.mobile.features.status.root.DefaultRootStatusComponent +import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent +import com.makeevrserg.empireprojekt.mobile.features.theme.DefaultThemeSwitcherComponentComponent +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent +import ru.astrainteractive.klibs.kdi.Provider +import ru.astrainteractive.klibs.kdi.Single +import ru.astrainteractive.klibs.kdi.getValue + +class RootModuleImpl : RootModule { + + override val servicesModule: ServicesModule by Single { + ServicesModuleImpl() + } + + override val statusModule: StatusModule by Provider { + StatusModule.Default( + dispatchers = servicesModule.dispatchers.value, + httpClient = servicesModule.httpClient.value + ) + } + + override val splashModule: SplashComponentModule by Provider { + SplashComponentModule.Default( + mainScope = servicesModule.mainScope.value, + dispatchers = servicesModule.dispatchers.value + ) + } + + override val rootStatusComponent: Single = Single { + DefaultRootStatusComponent(statusModule) + } + + override val themeSwitcherComponent: Single = Single { + DefaultThemeSwitcherComponentComponent(servicesModule.settings.value) + } +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/ServicesModuleImpl.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/ServicesModuleImpl.kt similarity index 69% rename from modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/ServicesModuleImpl.kt rename to modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/ServicesModuleImpl.kt index a81b897d..7cc316ad 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/ServicesModuleImpl.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/ServicesModuleImpl.kt @@ -1,18 +1,23 @@ -package com.makeevrserg.empireprojekt.mobile.features.root.di.impl.root +package com.makeevrserg.empireprojekt.mobile.features.root.di.impl import com.makeevrserg.empireprojekt.mobile.features.root.di.ServicesModule import com.makeevrserg.empireprojekt.mobile.features.root.di.factory.LinkBrowserFactory +import com.makeevrserg.empireprojekt.mobile.features.root.di.factory.SettingsFactory import com.makeevrserg.empireprojekt.mobile.services.core.LinkBrowser import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.MainScope import kotlinx.serialization.json.Json import ru.astrainteractive.klibs.kdi.Lateinit import ru.astrainteractive.klibs.kdi.Single import ru.astrainteractive.klibs.kdi.getValue +import ru.astrainteractive.klibs.mikro.core.dispatchers.DefaultKotlinDispatchers +import ru.astrainteractive.klibs.mikro.core.dispatchers.KotlinDispatchers import ru.astrainteractive.klibs.mikro.platform.PlatformConfiguration internal class ServicesModuleImpl : ServicesModule { + override val platformConfiguration = Lateinit() override val jsonConfiguration = Single { @@ -35,4 +40,17 @@ internal class ServicesModuleImpl : ServicesModule { override val linkBrowser: Single = Single { LinkBrowserFactory(platformConfiguration.value).create() } + + override val settings = Single { + val configuration by platformConfiguration + SettingsFactory(configuration).create() + } + + override val dispatchers = Single { + DefaultKotlinDispatchers + } + + override val mainScope = Single { + MainScope() + } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt deleted file mode 100644 index c172f96c..00000000 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.makeevrserg.empireprojekt.mobile.features.root.di.impl.root - -import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule -import com.makeevrserg.empireprojekt.mobile.features.root.di.factory.SettingsFactory -import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.status.StatusModuleImpl -import com.makeevrserg.empireprojekt.mobile.features.status.root.DefaultRootStatusComponent -import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent -import com.makeevrserg.empireprojekt.mobile.features.theme.DefaultThemeSwitcherComponentComponent -import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent -import kotlinx.coroutines.MainScope -import ru.astrainteractive.klibs.kdi.Single -import ru.astrainteractive.klibs.kdi.getValue -import ru.astrainteractive.klibs.mikro.core.dispatchers.DefaultKotlinDispatchers -import ru.astrainteractive.klibs.mikro.core.dispatchers.KotlinDispatchers - -internal object RootModuleImpl : RootModule { - override val servicesModule by Single { - ServicesModuleImpl() - } - - override val settings = Single { - val configuration by servicesModule.platformConfiguration - SettingsFactory(configuration).create() - } - - override val dispatchers = Single { - DefaultKotlinDispatchers - } - - override val mainScope = Single { - MainScope() - } - override val themeSwitcherComponent: Single = Single { - DefaultThemeSwitcherComponentComponent(settings.value) - } - override val rootStatusComponent: Single = Single { - DefaultRootStatusComponent(StatusModuleImpl(this)) - } -} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/splash/SplashComponentModuleImpl.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/splash/SplashComponentModuleImpl.kt deleted file mode 100644 index 39df9de4..00000000 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/splash/SplashComponentModuleImpl.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.makeevrserg.empireprojekt.mobile.features.root.di.impl.splash - -import com.makeevrserg.empireprojekt.mobile.features.logic.splash.data.SplashComponentRepository -import com.makeevrserg.empireprojekt.mobile.features.logic.splash.data.SplashComponentRepositoryImpl -import com.makeevrserg.empireprojekt.mobile.features.logic.splash.di.SplashComponentModule -import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule -import com.makeevrserg.empireprojekt.mobile.features.root.di.ServicesModule -import kotlinx.coroutines.CoroutineScope -import ru.astrainteractive.klibs.kdi.Provider -import ru.astrainteractive.klibs.kdi.getValue -import ru.astrainteractive.klibs.mikro.core.dispatchers.KotlinDispatchers - -@Suppress("UnusedPrivateMember") -class SplashComponentModuleImpl( - rootModule: RootModule, - servicesModule: ServicesModule -) : SplashComponentModule { - - override val scope: CoroutineScope by rootModule.mainScope - override val dispatchers: KotlinDispatchers by rootModule.dispatchers - override val repository: SplashComponentRepository = Provider { - SplashComponentRepositoryImpl() - }.provide() -} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/status/StatusModuleImpl.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/status/StatusModuleImpl.kt deleted file mode 100644 index 9abdd56d..00000000 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/status/StatusModuleImpl.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.makeevrserg.empireprojekt.mobile.features.root.di.impl.status - -import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule -import com.makeevrserg.empireprojekt.mobile.features.status.di.StatusModule -import io.ktor.client.HttpClient -import ru.astrainteractive.klibs.kdi.getValue -import ru.astrainteractive.klibs.mikro.core.dispatchers.KotlinDispatchers - -class StatusModuleImpl(rootModule: RootModule) : StatusModule { - private val servicesModule by rootModule.servicesModule - - override val dispatchers: KotlinDispatchers by rootModule.dispatchers - override val httpClient: HttpClient by servicesModule.httpClient -} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/di/factory/RootScreenComponentChildFactory.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/di/factory/RootScreenComponentChildFactory.kt index af744d33..965c2fb2 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/di/factory/RootScreenComponentChildFactory.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/di/factory/RootScreenComponentChildFactory.kt @@ -3,7 +3,6 @@ package com.makeevrserg.empireprojekt.mobile.features.root.screen.di.factory import com.arkivanov.decompose.ComponentContext import com.makeevrserg.empireprojekt.mobile.features.logic.splash.DefaultSplashComponent import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule -import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.splash.SplashComponentModuleImpl import com.makeevrserg.empireprojekt.mobile.features.root.screen.DefaultRootScreenComponent import com.makeevrserg.empireprojekt.mobile.features.root.screen.RootScreenComponent import ru.astrainteractive.klibs.kdi.Factory @@ -18,10 +17,7 @@ class RootScreenComponentChildFactory( RootScreenComponent.Child.Splash -> DefaultRootScreenComponent.Configuration.Splash( splashComponent = DefaultSplashComponent( context = context, - module = SplashComponentModuleImpl( - rootModule = rootModule, - servicesModule = rootModule.servicesModule - ) + module = rootModule.splashModule ) ) diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/di/StatusModule.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/di/StatusModule.kt index 1ddce066..c8b973d9 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/di/StatusModule.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/di/StatusModule.kt @@ -2,9 +2,15 @@ package com.makeevrserg.empireprojekt.mobile.features.status.di import io.ktor.client.HttpClient import ru.astrainteractive.klibs.kdi.Module +import ru.astrainteractive.klibs.kdi.getValue import ru.astrainteractive.klibs.mikro.core.dispatchers.KotlinDispatchers interface StatusModule : Module { val dispatchers: KotlinDispatchers val httpClient: HttpClient + + class Default( + override val dispatchers: KotlinDispatchers, + override val httpClient: HttpClient + ) : StatusModule } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt index 84722115..2d2bdd8f 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt @@ -5,19 +5,25 @@ import kotlinx.coroutines.flow.StateFlow import ru.astrainteractive.klibs.kstorage.StateFlowMutableStorageValue import ru.astrainteractive.klibs.mikro.core.util.next -class DefaultThemeSwitcherComponentComponent(private val settings: Settings) : ThemeSwitcherComponent { +class DefaultThemeSwitcherComponentComponent( + private val settings: Settings +) : ThemeSwitcherComponent { + private val key = "THEME" + private val default = ThemeSwitcherComponent.Theme.DARK + private val themeFlowStorageValue = StateFlowMutableStorageValue( default = default, loadSettingsValue = { val ordinal = settings.getInt(key, ThemeSwitcherComponent.Theme.LIGHT.ordinal) - ThemeSwitcherComponent.Theme.values().getOrNull(ordinal) ?: default + ThemeSwitcherComponent.Theme.entries.getOrNull(ordinal) ?: default }, saveSettingsValue = { settings.putInt(key, it.ordinal) } ) + override val theme: StateFlow = themeFlowStorageValue.stateFlow override fun selectDarkTheme() { @@ -33,7 +39,9 @@ class DefaultThemeSwitcherComponentComponent(private val settings: Settings) : T } override fun next() { - selectTheme(theme.value.next(ThemeSwitcherComponent.Theme.values())) + val entries = ThemeSwitcherComponent.Theme.entries.toTypedArray() + val nextTheme = theme.value.next(entries) + selectTheme(nextTheme) } init { diff --git a/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/DefaultSplashComponent.kt b/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/DefaultSplashComponent.kt index ff240c06..7aeb6145 100644 --- a/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/DefaultSplashComponent.kt +++ b/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/DefaultSplashComponent.kt @@ -17,7 +17,7 @@ class DefaultSplashComponent( override val screenChannelFlow = _screenChannel.consumeAsFlow().cFlow() init { - scope.launch(dispatchers.IO) { + mainScope.launch(dispatchers.IO) { val isInitialLaunch = repository.isInitialLaunch() val label = SplashComponent.Label.InitialLaunch(isInitialLaunch) _screenChannel.send(label) diff --git a/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/di/SplashComponentModule.kt b/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/di/SplashComponentModule.kt index 202f350c..1d052bf0 100644 --- a/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/di/SplashComponentModule.kt +++ b/modules/features/splash/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/di/SplashComponentModule.kt @@ -1,12 +1,24 @@ package com.makeevrserg.empireprojekt.mobile.features.logic.splash.di import com.makeevrserg.empireprojekt.mobile.features.logic.splash.data.SplashComponentRepository +import com.makeevrserg.empireprojekt.mobile.features.logic.splash.data.SplashComponentRepositoryImpl import kotlinx.coroutines.CoroutineScope import ru.astrainteractive.klibs.kdi.Module +import ru.astrainteractive.klibs.kdi.Provider +import ru.astrainteractive.klibs.kdi.getValue import ru.astrainteractive.klibs.mikro.core.dispatchers.KotlinDispatchers interface SplashComponentModule : Module { - val scope: CoroutineScope + val mainScope: CoroutineScope val dispatchers: KotlinDispatchers val repository: SplashComponentRepository + + class Default( + override val mainScope: CoroutineScope, + override val dispatchers: KotlinDispatchers + ) : SplashComponentModule { + override val repository: SplashComponentRepository = Provider { + SplashComponentRepositoryImpl() + }.provide() + } } diff --git a/modules/features/splash/src/commonTest/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/SplashComponentTest.kt b/modules/features/splash/src/commonTest/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/SplashComponentTest.kt index 9bdbe979..3281f66c 100644 --- a/modules/features/splash/src/commonTest/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/SplashComponentTest.kt +++ b/modules/features/splash/src/commonTest/kotlin/com/makeevrserg/empireprojekt/mobile/features/logic/splash/SplashComponentTest.kt @@ -16,7 +16,7 @@ import kotlin.test.assertTrue class SplashComponentTest { private fun buildModule(isInitialLaunch: Boolean) = object : SplashComponentModule { - override val scope: CoroutineScope = MainScope() + override val mainScope: CoroutineScope = MainScope() override val dispatchers: KotlinDispatchers = DefaultKotlinDispatchers override val repository: SplashComponentRepository = object : SplashComponentRepository { override fun isInitialLaunch(): Boolean = isInitialLaunch From 3f6e5cadd1f2b0f9a5d9dd914b1ef4942e931408 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Wed, 11 Oct 2023 21:29:27 +0300 Subject: [PATCH 14/20] refactor: update theme switcher module --- .../features/root/di/impl/RootModuleImpl.kt | 4 +- .../DefaultThemeSwitcherComponentComponent.kt | 38 +++++++------------ .../theme/PreviewThemeSwitcherComponent.kt | 13 ++++--- .../features/theme/ThemeSwitcherComponent.kt | 5 +-- .../theme/data/ThemeSwitcherRepository.kt | 9 +++++ .../theme/data/ThemeSwitcherRepositoryImpl.kt | 25 ++++++++++++ .../mobile/features/theme/data/model/Theme.kt | 5 +++ .../features/theme/di/ThemeSwitcherModule.kt | 18 +++++++++ .../features/ui/root/ComposeApplication.kt | 7 ++-- .../mobile/features/ui/status/StatusScreen.kt | 3 +- .../features/main/components/ThemeChip.kt | 9 +++-- 11 files changed, 93 insertions(+), 43 deletions(-) create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/ThemeSwitcherRepository.kt create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/ThemeSwitcherRepositoryImpl.kt create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/model/Theme.kt create mode 100644 modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/di/ThemeSwitcherModule.kt diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/RootModuleImpl.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/RootModuleImpl.kt index 219cac49..4f488cf3 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/RootModuleImpl.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/RootModuleImpl.kt @@ -8,6 +8,7 @@ import com.makeevrserg.empireprojekt.mobile.features.status.root.DefaultRootStat import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent import com.makeevrserg.empireprojekt.mobile.features.theme.DefaultThemeSwitcherComponentComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent +import com.makeevrserg.empireprojekt.mobile.features.theme.di.ThemeSwitcherModule import ru.astrainteractive.klibs.kdi.Provider import ru.astrainteractive.klibs.kdi.Single import ru.astrainteractive.klibs.kdi.getValue @@ -37,6 +38,7 @@ class RootModuleImpl : RootModule { } override val themeSwitcherComponent: Single = Single { - DefaultThemeSwitcherComponentComponent(servicesModule.settings.value) + val module = ThemeSwitcherModule.Default(servicesModule.settings.value) + DefaultThemeSwitcherComponentComponent(module) } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt index 2d2bdd8f..b20e16fb 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt @@ -1,45 +1,35 @@ package com.makeevrserg.empireprojekt.mobile.features.theme -import com.russhwolf.settings.Settings +import com.makeevrserg.empireprojekt.mobile.features.theme.data.model.Theme +import com.makeevrserg.empireprojekt.mobile.features.theme.di.ThemeSwitcherModule import kotlinx.coroutines.flow.StateFlow -import ru.astrainteractive.klibs.kstorage.StateFlowMutableStorageValue +import ru.astrainteractive.klibs.kdi.Provider +import ru.astrainteractive.klibs.kdi.getValue import ru.astrainteractive.klibs.mikro.core.util.next class DefaultThemeSwitcherComponentComponent( - private val settings: Settings -) : ThemeSwitcherComponent { - - private val key = "THEME" - - private val default = ThemeSwitcherComponent.Theme.DARK - - private val themeFlowStorageValue = StateFlowMutableStorageValue( - default = default, - loadSettingsValue = { - val ordinal = settings.getInt(key, ThemeSwitcherComponent.Theme.LIGHT.ordinal) - ThemeSwitcherComponent.Theme.entries.getOrNull(ordinal) ?: default - }, - saveSettingsValue = { - settings.putInt(key, it.ordinal) - } - ) + themeSwitcherModule: ThemeSwitcherModule +) : ThemeSwitcherComponent, ThemeSwitcherModule by themeSwitcherModule { + private val themeFlowStorageValue by Provider { + themeSwitcherRepository.themeFlowStorageValue + } - override val theme: StateFlow = themeFlowStorageValue.stateFlow + override val theme: StateFlow = themeFlowStorageValue.stateFlow override fun selectDarkTheme() { - themeFlowStorageValue.save(ThemeSwitcherComponent.Theme.DARK) + themeFlowStorageValue.save(Theme.DARK) } override fun selectLightTheme() { - themeFlowStorageValue.save(ThemeSwitcherComponent.Theme.LIGHT) + themeFlowStorageValue.save(Theme.LIGHT) } - override fun selectTheme(theme: ThemeSwitcherComponent.Theme) { + override fun selectTheme(theme: Theme) { themeFlowStorageValue.save(theme) } override fun next() { - val entries = ThemeSwitcherComponent.Theme.entries.toTypedArray() + val entries = Theme.entries.toTypedArray() val nextTheme = theme.value.next(entries) selectTheme(nextTheme) } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcherComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcherComponent.kt index 7a15d321..90764a65 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcherComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcherComponent.kt @@ -1,25 +1,26 @@ package com.makeevrserg.empireprojekt.mobile.features.theme +import com.makeevrserg.empireprojekt.mobile.features.theme.data.model.Theme import kotlinx.coroutines.flow.MutableStateFlow import ru.astrainteractive.klibs.mikro.core.util.next class PreviewThemeSwitcherComponent : ThemeSwitcherComponent { - override val theme: MutableStateFlow = - MutableStateFlow(ThemeSwitcherComponent.Theme.LIGHT) + override val theme: MutableStateFlow = + MutableStateFlow(Theme.LIGHT) override fun selectDarkTheme() { - selectTheme(ThemeSwitcherComponent.Theme.DARK) + selectTheme(Theme.DARK) } override fun selectLightTheme() { - selectTheme(ThemeSwitcherComponent.Theme.LIGHT) + selectTheme(Theme.LIGHT) } - override fun selectTheme(theme: ThemeSwitcherComponent.Theme) { + override fun selectTheme(theme: Theme) { this.theme.value = theme } override fun next() { - theme.value.next(ThemeSwitcherComponent.Theme.values()).run(::selectTheme) + theme.value.next(Theme.values()).run(::selectTheme) } } diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt index 4bb7e468..2ea51a08 100644 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcherComponent.kt @@ -1,14 +1,11 @@ package com.makeevrserg.empireprojekt.mobile.features.theme +import com.makeevrserg.empireprojekt.mobile.features.theme.data.model.Theme import kotlinx.coroutines.flow.StateFlow interface ThemeSwitcherComponent { val theme: StateFlow - enum class Theme { - DARK, LIGHT - } - fun selectDarkTheme() fun selectLightTheme() fun selectTheme(theme: Theme) diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/ThemeSwitcherRepository.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/ThemeSwitcherRepository.kt new file mode 100644 index 00000000..8eb0e480 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/ThemeSwitcherRepository.kt @@ -0,0 +1,9 @@ +package com.makeevrserg.empireprojekt.mobile.features.theme.data + +import com.makeevrserg.empireprojekt.mobile.features.theme.data.model.Theme +import ru.astrainteractive.klibs.kstorage.api.StateFlowMutableStorageValue + +interface ThemeSwitcherRepository { + + val themeFlowStorageValue: StateFlowMutableStorageValue +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/ThemeSwitcherRepositoryImpl.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/ThemeSwitcherRepositoryImpl.kt new file mode 100644 index 00000000..389b5842 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/ThemeSwitcherRepositoryImpl.kt @@ -0,0 +1,25 @@ +package com.makeevrserg.empireprojekt.mobile.features.theme.data + +import com.makeevrserg.empireprojekt.mobile.features.theme.data.model.Theme +import com.russhwolf.settings.Settings +import ru.astrainteractive.klibs.kstorage.StateFlowMutableStorageValue + +class ThemeSwitcherRepositoryImpl( + private val settings: Settings +) : ThemeSwitcherRepository { + + private val key = "THEME" + + private val default = Theme.DARK + + override val themeFlowStorageValue = StateFlowMutableStorageValue( + default = default, + loadSettingsValue = { + val ordinal = settings.getInt(key, Theme.LIGHT.ordinal) + Theme.entries.getOrNull(ordinal) ?: default + }, + saveSettingsValue = { + settings.putInt(key, it.ordinal) + } + ) +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/model/Theme.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/model/Theme.kt new file mode 100644 index 00000000..41deb31c --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/data/model/Theme.kt @@ -0,0 +1,5 @@ +package com.makeevrserg.empireprojekt.mobile.features.theme.data.model + +enum class Theme { + DARK, LIGHT +} diff --git a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/di/ThemeSwitcherModule.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/di/ThemeSwitcherModule.kt new file mode 100644 index 00000000..18a2acaf --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/di/ThemeSwitcherModule.kt @@ -0,0 +1,18 @@ +package com.makeevrserg.empireprojekt.mobile.features.theme.di + +import com.makeevrserg.empireprojekt.mobile.features.theme.data.ThemeSwitcherRepository +import com.makeevrserg.empireprojekt.mobile.features.theme.data.ThemeSwitcherRepositoryImpl +import com.russhwolf.settings.Settings +import ru.astrainteractive.klibs.kdi.Single +import ru.astrainteractive.klibs.kdi.getValue + +interface ThemeSwitcherModule { + + val themeSwitcherRepository: ThemeSwitcherRepository + + class Default(settings: Settings) : ThemeSwitcherModule { + override val themeSwitcherRepository: ThemeSwitcherRepository by Single { + ThemeSwitcherRepositoryImpl(settings) + } + } +} diff --git a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt index 70d1d3ac..a27b41f8 100644 --- a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt +++ b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/root/ComposeApplication.kt @@ -10,10 +10,11 @@ import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme import com.makeevrserg.empireprojekt.mobile.core.ui.theme.LocalAppTheme import com.makeevrserg.empireprojekt.mobile.features.theme.PreviewThemeSwitcherComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent +import com.makeevrserg.empireprojekt.mobile.features.theme.data.model.Theme -fun ThemeSwitcherComponent.Theme.toComposeTheme() = when (this) { - ThemeSwitcherComponent.Theme.DARK -> AppTheme.DefaultDarkTheme - ThemeSwitcherComponent.Theme.LIGHT -> AppTheme.DefaultLightTheme +fun Theme.toComposeTheme() = when (this) { + Theme.DARK -> AppTheme.DefaultDarkTheme + Theme.LIGHT -> AppTheme.DefaultLightTheme } @Composable diff --git a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt index 70379aef..2065cf14 100644 --- a/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt +++ b/modules/features/ui/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/ui/status/StatusScreen.kt @@ -22,6 +22,7 @@ import com.makeevrserg.empireprojekt.mobile.features.root.RootComponent import com.makeevrserg.empireprojekt.mobile.features.root.modal.RootBottomSheetComponent import com.makeevrserg.empireprojekt.mobile.features.status.root.RootStatusComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent +import com.makeevrserg.empireprojekt.mobile.features.theme.data.model.Theme import com.makeevrserg.empireprojekt.mobile.features.ui.status.widget.StatusWidget import com.makeevrserg.empireprojekt.mobile.resources.MR import ru.astrainteractive.klibs.mikro.core.util.next @@ -65,7 +66,7 @@ fun StatusScreen( .clip(CircleShape) .clickable { val nextTheme = themeSwitcherComponent.theme.value.next( - ThemeSwitcherComponent.Theme.values() + Theme.values() ) themeSwitcherComponent.selectTheme(nextTheme) } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt index 25b0cd85..e9710993 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt @@ -18,19 +18,20 @@ import androidx.wear.compose.material.Icon import androidx.wear.compose.material.Text import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent +import com.makeevrserg.empireprojekt.mobile.features.theme.data.model.Theme import com.makeevrserg.empireprojekt.mobile.wear.features.components.AstraChip @Composable fun ThemeChip(themeSwitcherComponent: ThemeSwitcherComponent) { val theme by themeSwitcherComponent.theme.collectAsState() val icon = when (theme) { - ThemeSwitcherComponent.Theme.DARK -> Icons.Filled.Bedtime - ThemeSwitcherComponent.Theme.LIGHT -> Icons.Filled.WbSunny + Theme.DARK -> Icons.Filled.Bedtime + Theme.LIGHT -> Icons.Filled.WbSunny } val color by animateColorAsState( targetValue = when (theme) { - ThemeSwitcherComponent.Theme.DARK -> AppTheme.materialColor.onPrimary - ThemeSwitcherComponent.Theme.LIGHT -> AppTheme.materialColor.onPrimary + Theme.DARK -> AppTheme.materialColor.onPrimary + Theme.LIGHT -> AppTheme.materialColor.onPrimary }, label = "LABEL" ) From dcc060d587f3e3536693f70c1a9f25c1e771b75b Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Wed, 11 Oct 2023 22:00:44 +0300 Subject: [PATCH 15/20] feat: wear messenger --- .gitignore | 1 + androidApp/build.gradle.kts | 5 ++ .../empireprojekt/mobile/application/App.kt | 13 --- .../mobile/work/CheckStatusWork.kt | 87 +++++++++---------- .../services/wear-messenger/build.gradle.kts | 36 ++++++++ .../api/app/message/StatusModelMessage.kt | 13 +++ .../messenger/api/app/model/StatusModel.kt | 14 +++ .../api/message/DecodedWearMessage.kt | 6 ++ .../api/message/InlineWearMessage.kt | 33 +++++++ .../wear/messenger/api/message/WearMessage.kt | 8 ++ .../api/producer/WearMessageProducer.kt | 7 ++ .../api/producer/WearMessageProducerImpl.kt | 42 +++++++++ .../api/receiver/WearMessageReceiver.kt | 10 +++ .../api/receiver/WearMessageReceiverImpl.kt | 39 +++++++++ settings.gradle.kts | 1 + 15 files changed, 254 insertions(+), 61 deletions(-) create mode 100644 modules/services/wear-messenger/build.gradle.kts create mode 100644 modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/message/StatusModelMessage.kt create mode 100644 modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/model/StatusModel.kt create mode 100644 modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/DecodedWearMessage.kt create mode 100644 modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/InlineWearMessage.kt create mode 100644 modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/WearMessage.kt create mode 100644 modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/producer/WearMessageProducer.kt create mode 100644 modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/producer/WearMessageProducerImpl.kt create mode 100644 modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/receiver/WearMessageReceiver.kt create mode 100644 modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/receiver/WearMessageReceiverImpl.kt diff --git a/.gitignore b/.gitignore index 44b5b08d..c2dd4aa1 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ modules/services/file_system/build modules/services/xlsx/build modules/services/xlsx/libs modules/services/ads-yandex/build +modules/services/wear-messenger/build # Features ---------------------------------- modules/features/dialog-confirm/build modules/features/words-local/build diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 2300e71a..826b84f9 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -92,6 +92,8 @@ android { } dependencies { + // Kotlin + implementation(libs.kotlin.serialization.json) // Coroutines implementation(libs.kotlin.coroutines.core) implementation(libs.kotlin.coroutines.android) @@ -117,6 +119,8 @@ dependencies { implementation(libs.klibs.mikro.platform) implementation(libs.klibs.kstorage) implementation(libs.klibs.kdi) + // moko + implementation(libs.moko.resources.core) // Decompose implementation(libs.decompose.core) implementation(libs.decompose.compose.jetpack) @@ -132,4 +136,5 @@ dependencies { implementation(projects.modules.features.ui) implementation(projects.modules.services.coreUi) implementation(projects.modules.services.resources) + implementation(projects.modules.services.wearMessenger) } diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt index 2ee4aeb1..30646e37 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt @@ -12,9 +12,6 @@ import com.google.firebase.ktx.initialize import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.RootModuleImpl import com.makeevrserg.empireprojekt.mobile.work.CheckStatusWork import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import ru.astrainteractive.klibs.kdi.getValue import ru.astrainteractive.klibs.mikro.platform.DefaultAndroidPlatformConfiguration import java.util.concurrent.TimeUnit @@ -62,16 +59,6 @@ class App : Application() { ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, statusWork ) - rootModule.servicesModule.mainScope.value.launch { - while (isActive) { - delay(5000L) - CheckStatusWork.sendMessageOnWear( - wearDataLayerRegistry = wearDataLayerRegistry, - rootModule = rootModule, - messageClient = messageClient - ) - } - } } companion object { diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt index 88b1ba76..a165e735 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt @@ -4,16 +4,16 @@ import android.content.Context import android.util.Log import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.google.android.gms.wearable.MessageClient import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.data.WearDataLayerRegistry import com.makeevrserg.empireprojekt.mobile.application.App.Companion.asEmpireApp -import com.makeevrserg.empireprojekt.mobile.features.root.di.RootModule import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.message.StatusModelMessage +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.model.StatusModel +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.producer.WearMessageProducerImpl import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.tasks.await import ru.astrainteractive.klibs.kdi.Provider import ru.astrainteractive.klibs.kdi.getValue @@ -22,6 +22,21 @@ class CheckStatusWork( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { + private val wearDataLayerRegistry by lazy { + WearDataLayerRegistry.fromContext( + application = applicationContext, + coroutineScope = rootModule.servicesModule.mainScope.value + ) + } + private val messageClient by lazy { + wearDataLayerRegistry.messageClient + } + private val wearMessageProducer by lazy { + WearMessageProducerImpl( + wearDataLayerRegistry = wearDataLayerRegistry, + messageClient = messageClient + ) + } private val rootModule by lazy { applicationContext.asEmpireApp().rootModule @@ -30,59 +45,35 @@ class CheckStatusWork( rootModule.rootStatusComponent.value } - override suspend fun doWork(): Result = coroutineScope { + override suspend fun doWork(): Result { Log.d(TAG, "doWork: ") - rootStatusComponent.statusComponents.map { + sendStatus() + return Result.success() + } + + private suspend fun sendStatus() = coroutineScope { + val messages = rootStatusComponent.statusComponents.map { async { it.checkStatus() + val model = it.model.value + StatusModel( + title = model.title.toString(applicationContext), + isLoading = model.isLoading, + status = when (model.status) { + StatusComponent.Model.LoadingStatus.LOADING -> StatusModel.LoadingStatus.LOADING + StatusComponent.Model.LoadingStatus.SUCCESS -> StatusModel.LoadingStatus.SUCCESS + StatusComponent.Model.LoadingStatus.ERROR -> StatusModel.LoadingStatus.ERROR + } + ) } }.awaitAll() - Result.success() + val statusModelMessage = StatusModelMessage( + json = rootModule.servicesModule.jsonConfiguration.value + ) + wearMessageProducer.produce(statusModelMessage, messages) } companion object { private const val TAG = "CheckStatusWork" - suspend fun sendMessageOnWear( - wearDataLayerRegistry: WearDataLayerRegistry, - rootModule: RootModule, - messageClient: MessageClient - ) = coroutineScope { - kotlin.runCatching { - val nodes = wearDataLayerRegistry.nodeClient.connectedNodes.await() - Log.d(TAG, "Contains ${nodes.size} nodes") - val mapped = rootModule.rootStatusComponent.value.statusComponents.map { - if (it.model.value.isLoading) { - StatusComponent.Model.LoadingStatus.LOADING - } else { - it.model.value.status - } - } - val statuses = buildList { - StatusComponent.Model.LoadingStatus.SUCCESS.let { status -> - status to mapped.count { it == status } - }.run(::add) - StatusComponent.Model.LoadingStatus.ERROR.let { status -> - status to mapped.count { it == status } - }.run(::add) - StatusComponent.Model.LoadingStatus.LOADING.let { status -> - status to mapped.count { it == status } - }.run(::add) - } - nodes.flatMap { node -> - statuses.map { entry -> - async { - messageClient.sendMessage( - node.id, - "/statuses" + entry.first.name, - byteArrayOf(entry.second.toByte()) - ) - } - } - }.awaitAll() - Log.d(TAG, "Sended ") - }.onFailure { - it.printStackTrace() - } - } } } diff --git a/modules/services/wear-messenger/build.gradle.kts b/modules/services/wear-messenger/build.gradle.kts new file mode 100644 index 00000000..12d14fec --- /dev/null +++ b/modules/services/wear-messenger/build.gradle.kts @@ -0,0 +1,36 @@ +@file:Suppress("UnusedPrivateMember") + +import ru.astrainteractive.gradleplugin.util.ProjectProperties.projectInfo + +plugins { + id("com.android.library") + kotlin("multiplatform") + id("ru.astrainteractive.gradleplugin.java.core") + id("ru.astrainteractive.gradleplugin.android.core") + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + android() + sourceSets { + val commonMain by getting { + dependencies { + // Kotlin + implementation(libs.kotlin.serialization.json) + // klibs + implementation(libs.klibs.mikro.core) + implementation(libs.klibs.mikro.platform) + implementation(libs.klibs.kstorage) + implementation(libs.klibs.kdi) + // horologist + implementation("com.google.android.horologist:horologist-datalayer:0.5.3") + // Coroutines + implementation(libs.kotlin.coroutines.core) + implementation(libs.kotlin.coroutines.playServices) + } + } + } +} +android { + namespace = "${projectInfo.group}.wear.messenger" +} diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/message/StatusModelMessage.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/message/StatusModelMessage.kt new file mode 100644 index 00000000..66060c8b --- /dev/null +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/message/StatusModelMessage.kt @@ -0,0 +1,13 @@ +package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.message + +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.model.StatusModel +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.InlineWearMessage +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.WearMessage +import kotlinx.serialization.json.Json + +class StatusModelMessage( + private val json: Json +) : WearMessage> by InlineWearMessage( + json = json, + path = "/status" +) diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/model/StatusModel.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/model/StatusModel.kt new file mode 100644 index 00000000..b80e6fe8 --- /dev/null +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/model/StatusModel.kt @@ -0,0 +1,14 @@ +package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.model + +import kotlinx.serialization.Serializable + +@Serializable +class StatusModel( + val title: String, + val isLoading: Boolean, + val status: LoadingStatus +) { + enum class LoadingStatus { + LOADING, SUCCESS, ERROR + } +} diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/DecodedWearMessage.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/DecodedWearMessage.kt new file mode 100644 index 00000000..a4e602f3 --- /dev/null +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/DecodedWearMessage.kt @@ -0,0 +1,6 @@ +package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message + +data class DecodedWearMessage( + val path: String, + val value: T +) diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/InlineWearMessage.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/InlineWearMessage.kt new file mode 100644 index 00000000..2c290f2c --- /dev/null +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/InlineWearMessage.kt @@ -0,0 +1,33 @@ +package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message + +import android.util.Log +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class InlineWearMessage( + override val path: String, + private val encode: (T) -> ByteArray, + private val decode: (ByteArray) -> T +) : WearMessage { + override fun encode(value: T): ByteArray = this.encode.invoke(value) + + override fun decode(byteArray: ByteArray): T = this.decode.invoke(byteArray) +} + +@Suppress("FunctionNaming") +inline fun InlineWearMessage( + json: Json, + path: String +): WearMessage = InlineWearMessage( + path = path, + encode = { value -> + val string = json.encodeToString(value) + Log.d("InlineWearMessage", "InlineWearMessage->encode: $string") + string.toByteArray() + }, + decode = { byteArray -> + val string = byteArray.decodeToString() + Log.d("InlineWearMessage", "InlineWearMessage->decode: $string") + json.decodeFromString(string) + } +) diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/WearMessage.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/WearMessage.kt new file mode 100644 index 00000000..d8de0528 --- /dev/null +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/message/WearMessage.kt @@ -0,0 +1,8 @@ +package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message + +interface WearMessage { + val path: String + + fun encode(value: T): ByteArray + fun decode(byteArray: ByteArray): T +} diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/producer/WearMessageProducer.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/producer/WearMessageProducer.kt new file mode 100644 index 00000000..b182db85 --- /dev/null +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/producer/WearMessageProducer.kt @@ -0,0 +1,7 @@ +package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.producer + +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.WearMessage + +interface WearMessageProducer { + suspend fun produce(message: WearMessage, value: T) +} diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/producer/WearMessageProducerImpl.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/producer/WearMessageProducerImpl.kt new file mode 100644 index 00000000..9cffc192 --- /dev/null +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/producer/WearMessageProducerImpl.kt @@ -0,0 +1,42 @@ +package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.producer + +import android.util.Log +import com.google.android.gms.wearable.MessageClient +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.data.WearDataLayerRegistry +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.WearMessage +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.tasks.await + +@OptIn(ExperimentalHorologistApi::class) +class WearMessageProducerImpl( + private val wearDataLayerRegistry: WearDataLayerRegistry, + private val messageClient: MessageClient, +) : WearMessageProducer { + override suspend fun produce(message: WearMessage, value: T): Unit = coroutineScope { + val nodes = wearDataLayerRegistry.nodeClient.connectedNodes.await() + Log.d(TAG, "produce: found ${nodes.size} nodes") + kotlin.runCatching { + val byteArray = message.encode(value) + nodes.map { + async { + messageClient.sendMessage( + it.id, + message.path, + byteArray + ) + } + }.awaitAll() + }.onFailure { + Log.e(TAG, "produce: failed to send message ${it.stackTraceToString()}") + }.onSuccess { + Log.d(TAG, "produce: message sent") + } + } + + companion object { + private const val TAG = "WearMessageProducer" + } +} diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/receiver/WearMessageReceiver.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/receiver/WearMessageReceiver.kt new file mode 100644 index 00000000..f98130c1 --- /dev/null +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/receiver/WearMessageReceiver.kt @@ -0,0 +1,10 @@ +package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.receiver + +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.DecodedWearMessage +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.WearMessage +import kotlinx.coroutines.flow.Flow + +interface WearMessageReceiver { + val messagesFlow: Flow> + suspend fun consume(message: WearMessage, byteArray: ByteArray) +} diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/receiver/WearMessageReceiverImpl.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/receiver/WearMessageReceiverImpl.kt new file mode 100644 index 00000000..ce05328c --- /dev/null +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/receiver/WearMessageReceiverImpl.kt @@ -0,0 +1,39 @@ +package com.makeevrserg.empireprojekt.mobile.wear.messenger.api.receiver + +import android.util.Log +import com.google.android.gms.wearable.MessageClient +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.data.WearDataLayerRegistry +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.DecodedWearMessage +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.WearMessage +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow + +@OptIn(ExperimentalHorologistApi::class) +@Suppress("UnusedPrivateMember") +class WearMessageReceiverImpl( + private val wearDataLayerRegistry: WearDataLayerRegistry, + private val messageClient: MessageClient, +) : WearMessageReceiver { + private val messageChannel = Channel>() + override val messagesFlow: Flow> = messageChannel.receiveAsFlow() + + override suspend fun consume(message: WearMessage, byteArray: ByteArray) { + kotlin.runCatching { + val decodedWearMessage = DecodedWearMessage( + path = message.path, + value = message.decode(byteArray) + ) + messageChannel.send(decodedWearMessage) + }.onFailure { + Log.d(TAG, "consume: could not publish message: ${it.stackTraceToString()}") + }.onSuccess { + Log.d(TAG, "consume: published message") + } + } + + companion object { + private const val TAG = "WearMessageReceiver" + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 244212a0..5164e99e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,6 +27,7 @@ include(":wearApp") include(":modules:services:resources") include(":modules:services:core-ui") include(":modules:services:core") +include(":modules:services:wear-messenger") // Feature include(":modules:features:root") include(":modules:features:splash") From ff537dd4b70869b9d291de205b643d99fbd4d68e Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Wed, 11 Oct 2023 22:05:51 +0300 Subject: [PATCH 16/20] refactor: add di into wear-messenger --- .../empireprojekt/mobile/application/App.kt | 8 ++++ .../mobile/work/CheckStatusWork.kt | 28 ++--------- .../wear/messenger/di/WearMessengerModule.kt | 46 +++++++++++++++++++ 3 files changed, 59 insertions(+), 23 deletions(-) create mode 100644 modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/di/WearMessengerModule.kt diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt index 30646e37..023a371c 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt @@ -10,6 +10,7 @@ import com.google.android.horologist.data.WearDataLayerRegistry import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.initialize import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.RootModuleImpl +import com.makeevrserg.empireprojekt.mobile.wear.messenger.di.WearMessengerModule import com.makeevrserg.empireprojekt.mobile.work.CheckStatusWork import kotlinx.coroutines.cancel import ru.astrainteractive.klibs.kdi.getValue @@ -21,6 +22,13 @@ class App : Application() { val rootModule by lazy { RootModuleImpl() } + val wearMessengerModule by lazy { + WearMessengerModule.Default( + context = rootModule.servicesModule.platformConfiguration.value.applicationContext, + coroutineScope = rootModule.servicesModule.mainScope.value, + json = rootModule.servicesModule.jsonConfiguration.value + ) + } private val wearDataLayerRegistry by lazy { WearDataLayerRegistry.fromContext( application = applicationContext, diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt index a165e735..5a6bcb3e 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt @@ -4,40 +4,22 @@ import android.content.Context import android.util.Log import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.google.android.horologist.annotations.ExperimentalHorologistApi -import com.google.android.horologist.data.WearDataLayerRegistry import com.makeevrserg.empireprojekt.mobile.application.App.Companion.asEmpireApp import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent -import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.message.StatusModelMessage import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.model.StatusModel -import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.producer.WearMessageProducerImpl import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import ru.astrainteractive.klibs.kdi.Provider import ru.astrainteractive.klibs.kdi.getValue -@OptIn(ExperimentalHorologistApi::class) class CheckStatusWork( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { - private val wearDataLayerRegistry by lazy { - WearDataLayerRegistry.fromContext( - application = applicationContext, - coroutineScope = rootModule.servicesModule.mainScope.value - ) - } - private val messageClient by lazy { - wearDataLayerRegistry.messageClient + private val wearMessengerModule by lazy { + applicationContext.asEmpireApp().wearMessengerModule } - private val wearMessageProducer by lazy { - WearMessageProducerImpl( - wearDataLayerRegistry = wearDataLayerRegistry, - messageClient = messageClient - ) - } - private val rootModule by lazy { applicationContext.asEmpireApp().rootModule } @@ -67,10 +49,10 @@ class CheckStatusWork( ) } }.awaitAll() - val statusModelMessage = StatusModelMessage( - json = rootModule.servicesModule.jsonConfiguration.value + wearMessengerModule.wearMessageProducer.produce( + message = wearMessengerModule.statusModelMessage, + value = messages ) - wearMessageProducer.produce(statusModelMessage, messages) } companion object { diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/di/WearMessengerModule.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/di/WearMessengerModule.kt new file mode 100644 index 00000000..90a0ffc0 --- /dev/null +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/di/WearMessengerModule.kt @@ -0,0 +1,46 @@ +package com.makeevrserg.empireprojekt.mobile.wear.messenger.di + +import android.content.Context +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.data.WearDataLayerRegistry +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.message.StatusModelMessage +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.producer.WearMessageProducer +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.producer.WearMessageProducerImpl +import kotlinx.coroutines.CoroutineScope +import kotlinx.serialization.json.Json + +interface WearMessengerModule { + val wearMessageProducer: WearMessageProducer + + val statusModelMessage: StatusModelMessage + + class Default( + context: Context, + coroutineScope: CoroutineScope, + json: Json + ) : WearMessengerModule { + @OptIn(ExperimentalHorologistApi::class) + private val wearDataLayerRegistry by lazy { + WearDataLayerRegistry.fromContext( + application = context, + coroutineScope = coroutineScope + ) + } + + @OptIn(ExperimentalHorologistApi::class) + private val messageClient by lazy { + wearDataLayerRegistry.messageClient + } + + @OptIn(ExperimentalHorologistApi::class) + override val wearMessageProducer: WearMessageProducer by lazy { + WearMessageProducerImpl( + wearDataLayerRegistry = wearDataLayerRegistry, + messageClient = messageClient + ) + } + override val statusModelMessage: StatusModelMessage = StatusModelMessage( + json = json + ) + } +} From 4114977c520366bc6ef0a0761303e06110edb8d0 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Wed, 11 Oct 2023 22:42:51 +0300 Subject: [PATCH 17/20] refactor: update wear app --- .../api/app/message/StatusModelMessage.kt | 2 +- .../wear/messenger/di/WearMessengerModule.kt | 11 ++++++ wearApp/build.gradle.kts | 5 +++ wearApp/src/main/AndroidManifest.xml | 2 +- .../empireprojekt/mobile/wear/App.kt | 16 -------- .../empireprojekt/mobile/wear/MainActivity.kt | 17 ++++++--- .../mobile/wear/application/App.kt | 25 ++++++++++++ .../mobile/wear/di/WearRootModule.kt | 7 ++-- .../mobile/wear/di/impl/WearRootModuleImpl.kt | 38 +++++++++++++++++-- .../mobile/wear/features/main/MainScreen.kt | 2 +- .../main/preview/RootScreenPreview.kt | 17 +++++++-- .../wear/features/navigation/RootComponent.kt | 9 ----- .../NavHostRootComponent.kt | 2 +- .../wear/features/root/RootComponent.kt | 9 +++++ .../RootScreen.kt} | 11 ++++-- .../status/DefaultWearStatusComponent.kt | 35 +++++++++++++++++ .../wear/features/status/StatusesScreen.kt | 16 -------- .../features/status/WearStatusComponent.kt | 2 - .../wear/service/DataLayerListenerService.kt | 30 ++++++++------- .../mobile/wear/tile/MainTileService.kt | 8 ++-- 20 files changed, 182 insertions(+), 82 deletions(-) delete mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/App.kt create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/application/App.kt delete mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/RootComponent.kt rename wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/{navigation => root}/NavHostRootComponent.kt (78%) create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/RootComponent.kt rename wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/{navigation/NavigationScreen.kt => root/RootScreen.kt} (72%) create mode 100644 wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/DefaultWearStatusComponent.kt diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/message/StatusModelMessage.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/message/StatusModelMessage.kt index 66060c8b..99b364e6 100644 --- a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/message/StatusModelMessage.kt +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/api/app/message/StatusModelMessage.kt @@ -9,5 +9,5 @@ class StatusModelMessage( private val json: Json ) : WearMessage> by InlineWearMessage( json = json, - path = "/status" + path = "/statuses" ) diff --git a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/di/WearMessengerModule.kt b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/di/WearMessengerModule.kt index 90a0ffc0..98025055 100644 --- a/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/di/WearMessengerModule.kt +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/di/WearMessengerModule.kt @@ -6,11 +6,14 @@ import com.google.android.horologist.data.WearDataLayerRegistry import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.message.StatusModelMessage import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.producer.WearMessageProducer import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.producer.WearMessageProducerImpl +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.receiver.WearMessageReceiver +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.receiver.WearMessageReceiverImpl import kotlinx.coroutines.CoroutineScope import kotlinx.serialization.json.Json interface WearMessengerModule { val wearMessageProducer: WearMessageProducer + val wearMessageReceiver: WearMessageReceiver val statusModelMessage: StatusModelMessage @@ -39,6 +42,14 @@ interface WearMessengerModule { messageClient = messageClient ) } + + @OptIn(ExperimentalHorologistApi::class) + override val wearMessageReceiver: WearMessageReceiver by lazy { + WearMessageReceiverImpl( + wearDataLayerRegistry = wearDataLayerRegistry, + messageClient = messageClient + ) + } override val statusModelMessage: StatusModelMessage = StatusModelMessage( json = json ) diff --git a/wearApp/build.gradle.kts b/wearApp/build.gradle.kts index f8b0f52d..3a7b139e 100644 --- a/wearApp/build.gradle.kts +++ b/wearApp/build.gradle.kts @@ -84,6 +84,8 @@ android { } dependencies { + // Kotlin + implementation(libs.kotlin.serialization.json) // Coroutines implementation(libs.kotlin.coroutines.core) implementation(libs.kotlin.coroutines.android) @@ -115,6 +117,8 @@ dependencies { implementation(libs.klibs.kdi) // Settings implementation(libs.mppsettings) + // moko + implementation(libs.moko.resources.core) // Decompose implementation(libs.decompose.core) implementation(libs.decompose.compose.jetpack) @@ -125,4 +129,5 @@ dependencies { implementation(projects.modules.features.ui) implementation(projects.modules.services.coreUi) implementation(projects.modules.services.resources) + implementation(projects.modules.services.wearMessenger) } diff --git a/wearApp/src/main/AndroidManifest.xml b/wearApp/src/main/AndroidManifest.xml index 2f6804b7..b3d6285a 100644 --- a/wearApp/src/main/AndroidManifest.xml +++ b/wearApp/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ val settings: Single + val jsonConfiguration: Single val themeSwitcherComponent: Single val wearStatusComponent: Single - - companion object : WearRootModule by WearRootModuleImpl + val wearMessengerModule: WearMessengerModule } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt index 6c02bb5c..abf11495 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt @@ -3,23 +3,55 @@ package com.makeevrserg.empireprojekt.mobile.wear.di.impl import com.makeevrserg.empireprojekt.mobile.features.root.di.factory.SettingsFactory import com.makeevrserg.empireprojekt.mobile.features.theme.DefaultThemeSwitcherComponentComponent import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent +import com.makeevrserg.empireprojekt.mobile.features.theme.di.ThemeSwitcherModule +import com.makeevrserg.empireprojekt.mobile.services.core.CoroutineFeature import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import com.makeevrserg.empireprojekt.mobile.wear.features.status.DefaultWearStatusComponent import com.makeevrserg.empireprojekt.mobile.wear.features.status.WearStatusComponent +import com.makeevrserg.empireprojekt.mobile.wear.messenger.di.WearMessengerModule +import kotlinx.coroutines.MainScope +import kotlinx.serialization.json.Json import ru.astrainteractive.klibs.kdi.Lateinit import ru.astrainteractive.klibs.kdi.Single import ru.astrainteractive.klibs.kdi.getValue import ru.astrainteractive.klibs.mikro.platform.PlatformConfiguration -object WearRootModuleImpl : WearRootModule { +class WearRootModuleImpl : WearRootModule { override val platformConfiguration: Lateinit = Lateinit() + override val settings = Single { val configuration by platformConfiguration SettingsFactory(configuration).create() } + private val mainScope by Single { + MainScope() + } + + override val jsonConfiguration = Single { + Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + } + } + override val themeSwitcherComponent: Single = Single { - DefaultThemeSwitcherComponentComponent(settings.value) + val module = ThemeSwitcherModule.Default(settings.value) + DefaultThemeSwitcherComponentComponent(module) + } + + override val wearMessengerModule: WearMessengerModule by Single { + WearMessengerModule.Default( + context = platformConfiguration.value.applicationContext, + coroutineScope = CoroutineFeature.Default(), + json = jsonConfiguration.value + ) } + override val wearStatusComponent: Single = Single { - WearStatusComponent.Stub() + DefaultWearStatusComponent( + wearMessageReceiver = wearMessengerModule.wearMessageReceiver, + coroutineScope = mainScope + ) } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt index 60336f3d..3fe3a7e2 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt @@ -17,7 +17,7 @@ import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule import com.makeevrserg.empireprojekt.mobile.wear.features.main.components.NavChip import com.makeevrserg.empireprojekt.mobile.wear.features.main.components.ThemeChip -import com.makeevrserg.empireprojekt.mobile.wear.features.navigation.NavHostRootComponent +import com.makeevrserg.empireprojekt.mobile.wear.features.root.NavHostRootComponent import ru.astrainteractive.klibs.kdi.getValue @Composable diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt index f83384d1..425b6c5e 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt @@ -1,19 +1,30 @@ package com.makeevrserg.empireprojekt.mobile.wear.features.main.preview import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import com.makeevrserg.empireprojekt.mobile.features.ui.root.ComposeApplication -import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import com.makeevrserg.empireprojekt.mobile.wear.di.impl.WearRootModuleImpl import com.makeevrserg.empireprojekt.mobile.wear.features.main.MainScreen -import com.makeevrserg.empireprojekt.mobile.wear.features.navigation.NavHostRootComponent +import com.makeevrserg.empireprojekt.mobile.wear.features.root.NavHostRootComponent +import ru.astrainteractive.klibs.mikro.platform.DefaultAndroidPlatformConfiguration @Preview @Composable private fun RootScreenPreview() { val navController = rememberSwipeDismissableNavController() val navHostRootComponent = NavHostRootComponent(navController) + val context = LocalContext.current + val wearRootModule = WearRootModuleImpl().apply { + platformConfiguration.initialize { + DefaultAndroidPlatformConfiguration(context) + } + } ComposeApplication { - MainScreen(wearRootModule = WearRootModule, rootComponent = navHostRootComponent) + MainScreen( + wearRootModule = wearRootModule, + rootComponent = navHostRootComponent + ) } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/RootComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/RootComponent.kt deleted file mode 100644 index 778fab07..00000000 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/RootComponent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.makeevrserg.empireprojekt.mobile.wear.features.navigation - -interface RootComponent { - fun openStatuses() - sealed interface Child { - object Main : Child - object Statuses : Child - } -} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavHostRootComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/NavHostRootComponent.kt similarity index 78% rename from wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavHostRootComponent.kt rename to wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/NavHostRootComponent.kt index 4290df20..fc1416f4 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavHostRootComponent.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/NavHostRootComponent.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.wear.features.navigation +package com.makeevrserg.empireprojekt.mobile.wear.features.root import androidx.navigation.NavHostController diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/RootComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/RootComponent.kt new file mode 100644 index 00000000..2b277aec --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/RootComponent.kt @@ -0,0 +1,9 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.root + +interface RootComponent { + fun openStatuses() + sealed interface Child { + data object Main : Child + data object Statuses : Child + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavigationScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/RootScreen.kt similarity index 72% rename from wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavigationScreen.kt rename to wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/RootScreen.kt index da8a292f..16e00b01 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/navigation/NavigationScreen.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/RootScreen.kt @@ -1,4 +1,4 @@ -package com.makeevrserg.empireprojekt.mobile.wear.features.navigation +package com.makeevrserg.empireprojekt.mobile.wear.features.root import androidx.compose.runtime.Composable import androidx.wear.compose.navigation.SwipeDismissableNavHost @@ -8,16 +8,19 @@ import com.makeevrserg.empireprojekt.mobile.wear.features.main.MainScreen import com.makeevrserg.empireprojekt.mobile.wear.features.status.StatusesScreen @Composable -fun NavigationScreen(rootComponent: NavHostRootComponent) { +fun RootScreen( + rootComponent: NavHostRootComponent, + wearRootModule: WearRootModule +) { SwipeDismissableNavHost( navController = rootComponent.navController, startDestination = RootComponent.Child.Main::class.simpleName!! ) { composable(RootComponent.Child.Main::class.simpleName!!) { - MainScreen(wearRootModule = WearRootModule, rootComponent = rootComponent) + MainScreen(wearRootModule = wearRootModule, rootComponent = rootComponent) } composable(RootComponent.Child.Statuses::class.simpleName!!) { - StatusesScreen(WearRootModule.wearStatusComponent.value) + StatusesScreen(wearRootModule.wearStatusComponent.value) } } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/DefaultWearStatusComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/DefaultWearStatusComponent.kt new file mode 100644 index 00000000..a17d8cb1 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/DefaultWearStatusComponent.kt @@ -0,0 +1,35 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.status + +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.app.model.StatusModel +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.message.DecodedWearMessage +import com.makeevrserg.empireprojekt.mobile.wear.messenger.api.receiver.WearMessageReceiver +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class DefaultWearStatusComponent( + wearMessageReceiver: WearMessageReceiver, + coroutineScope: CoroutineScope +) : WearStatusComponent { + override val mergedState: StateFlow = + wearMessageReceiver.messagesFlow + .filterIsInstance>>() + .map { it.value } + .map { statusModels -> + WearStatusComponent.Model( + loadingCount = statusModels.count { + it.isLoading || it.status == StatusModel.LoadingStatus.LOADING + }, + successCount = statusModels.count { it.status == StatusModel.LoadingStatus.SUCCESS }, + failureCount = statusModels.count { it.status == StatusModel.LoadingStatus.ERROR } + ) + }.stateIn(coroutineScope, SharingStarted.Eagerly, WearStatusComponent.Model()) + + override fun update(status: StatusComponent.Model.LoadingStatus, amount: Int) { + // todo + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt index 0eb5d75a..cfa3aed5 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt @@ -13,16 +13,13 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.wear.compose.foundation.lazy.AutoCenteringParams import androidx.wear.compose.foundation.lazy.ScalingLazyColumn -import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState import androidx.wear.compose.material.PositionIndicator import androidx.wear.compose.material.Scaffold -import androidx.wear.compose.material.Text import androidx.wear.compose.material.Vignette import androidx.wear.compose.material.VignettePosition import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme import com.makeevrserg.empireprojekt.mobile.wear.features.components.IconTextChip -import com.makeevrserg.empireprojekt.mobile.wear.features.status.components.StatusWidget @Composable fun StatusesScreen(wearStatusComponent: WearStatusComponent) { @@ -68,19 +65,6 @@ fun StatusesScreen(wearStatusComponent: WearStatusComponent) { iconColor = AppTheme.alColors.colorNegative ) } - - if (wearStatusComponent.statuses.isEmpty()) { - item { - Text( - text = "No items present", - style = AppTheme.typography.caption, - color = AppTheme.materialColor.onPrimary - ) - } - } - items(wearStatusComponent.statuses) { - StatusWidget(it) - } } } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt index d4cd7470..3ba4234f 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt @@ -6,7 +6,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update interface WearStatusComponent { - val statuses: List val mergedState: StateFlow data class Model( @@ -18,7 +17,6 @@ interface WearStatusComponent { fun update(status: StatusComponent.Model.LoadingStatus, amount: Int) class Stub : WearStatusComponent { - override val statuses: List = emptyList() private val mutableMergeState = MutableStateFlow(Model()) override val mergedState: StateFlow = mutableMergeState override fun update(status: StatusComponent.Model.LoadingStatus, amount: Int) { diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/service/DataLayerListenerService.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/service/DataLayerListenerService.kt index da97d7c9..4391bea9 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/service/DataLayerListenerService.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/service/DataLayerListenerService.kt @@ -3,31 +3,33 @@ package com.makeevrserg.empireprojekt.mobile.wear.service import android.util.Log import com.google.android.gms.wearable.MessageEvent import com.google.android.gms.wearable.WearableListenerService -import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent -import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import com.makeevrserg.empireprojekt.mobile.wear.application.App.Companion.asEmpireApp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch class DataLayerListenerService : WearableListenerService() { + private val wearRootModule by lazy { + application.asEmpireApp().wearRootModule + } private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) override fun onMessageReceived(messageEvent: MessageEvent) { super.onMessageReceived(messageEvent) + Log.d(TAG, "onMessageReceived: ${messageEvent.path}") + scope.launch { receiveStatusModelMessage(messageEvent) } + } - kotlin.runCatching { - val statusRaw = messageEvent.path.replace("/statuses", "") - val status = StatusComponent.Model.LoadingStatus.valueOf(statusRaw) - val amount = messageEvent.data.first().toInt() - WearRootModule.wearStatusComponent.value.update(status, amount) - Log.d("DataLayerService", "loadingStatus: $status; amount: $amount") - }.onFailure { - it.printStackTrace() - } - kotlin.runCatching { - Log.d(TAG, "onMessageReceived: ${messageEvent.data.decodeToString()}") - }.onFailure { it.printStackTrace() } + private suspend fun receiveStatusModelMessage(messageEvent: MessageEvent) { + val statusModelMessage = wearRootModule.wearMessengerModule.statusModelMessage + val wearMessageReceiver = wearRootModule.wearMessengerModule.wearMessageReceiver + if (statusModelMessage.path != messageEvent.path) return + wearMessageReceiver.consume( + message = statusModelMessage, + byteArray = messageEvent.data + ) } override fun onCreate() { diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt index 83ca7088..9b8d8234 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt @@ -14,7 +14,7 @@ import androidx.wear.tiles.TileService import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.tiles.SuspendingTileService import com.makeevrserg.empireprojekt.mobile.resources.R -import com.makeevrserg.empireprojekt.mobile.wear.di.WearRootModule +import com.makeevrserg.empireprojekt.mobile.wear.application.App.Companion.asEmpireApp import com.makeevrserg.empireprojekt.mobile.wear.tile.components.MainTileRenderer import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -30,8 +30,10 @@ private const val RESOURCES_VERSION = "1" @OptIn(ExperimentalHorologistApi::class) class MainTileService : SuspendingTileService() { - private val rootModule by WearRootModule - private val wearStatusComponent by rootModule.wearStatusComponent + private val wearRootModule by lazy { + application.asEmpireApp().wearRootModule + } + private val wearStatusComponent by wearRootModule.wearStatusComponent private val mainTileRenderer by Single { MainTileRenderer(applicationContext) } From 426658bda4f868ba8881e26a0c4a96455d7a17c8 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Thu, 12 Oct 2023 00:02:42 +0300 Subject: [PATCH 18/20] feat: add timestamp --- .../status/DefaultWearStatusComponent.kt | 13 ++++++++++++- .../wear/features/status/StatusesScreen.kt | 11 +++++++++++ .../wear/features/status/WearStatusComponent.kt | 4 ++-- ...ainTileService.kt => StatusesTileService.kt} | 11 +++++++---- .../wear/tile/components/MainTileRenderer.kt | 17 ++++++++++++++++- 5 files changed, 48 insertions(+), 8 deletions(-) rename wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/{MainTileService.kt => StatusesTileService.kt} (92%) diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/DefaultWearStatusComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/DefaultWearStatusComponent.kt index a17d8cb1..7bf5794e 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/DefaultWearStatusComponent.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/DefaultWearStatusComponent.kt @@ -10,6 +10,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter class DefaultWearStatusComponent( wearMessageReceiver: WearMessageReceiver, @@ -20,15 +22,24 @@ class DefaultWearStatusComponent( .filterIsInstance>>() .map { it.value } .map { statusModels -> + WearStatusComponent.Model( loadingCount = statusModels.count { it.isLoading || it.status == StatusModel.LoadingStatus.LOADING }, successCount = statusModels.count { it.status == StatusModel.LoadingStatus.SUCCESS }, - failureCount = statusModels.count { it.status == StatusModel.LoadingStatus.ERROR } + failureCount = statusModels.count { it.status == StatusModel.LoadingStatus.ERROR }, + updatedAt = getTimeStamp() ) }.stateIn(coroutineScope, SharingStarted.Eagerly, WearStatusComponent.Model()) + private fun getTimeStamp(): String { + val lDateTime = LocalDateTime.now() + val formatter = DateTimeFormatter.ofPattern("HH:mm:ss") + val formatted = lDateTime.format(formatter) + return formatted ?: "..." + } + override fun update(status: StatusComponent.Model.LoadingStatus, amount: Int) { // todo } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt index cfa3aed5..bd7977cb 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt @@ -11,11 +11,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.wear.compose.foundation.lazy.AutoCenteringParams import androidx.wear.compose.foundation.lazy.ScalingLazyColumn import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState import androidx.wear.compose.material.PositionIndicator import androidx.wear.compose.material.Scaffold +import androidx.wear.compose.material.Text import androidx.wear.compose.material.Vignette import androidx.wear.compose.material.VignettePosition import com.makeevrserg.empireprojekt.mobile.core.ui.theme.AppTheme @@ -41,6 +43,15 @@ fun StatusesScreen(wearStatusComponent: WearStatusComponent) { modifier = Modifier.fillMaxSize(), autoCentering = AutoCenteringParams(itemIndex = 0), ) { + item { + Text( + text = mergedState.updatedAt, + style = AppTheme.typography.caption, + color = AppTheme.materialColor.onPrimary, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } item { IconTextChip( modifier = Modifier.fillMaxWidth(), diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt index 3ba4234f..7c5a7df4 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt @@ -7,11 +7,11 @@ import kotlinx.coroutines.flow.update interface WearStatusComponent { val mergedState: StateFlow - data class Model( val loadingCount: Int = 0, val successCount: Int = 0, - val failureCount: Int = 0 + val failureCount: Int = 0, + val updatedAt: String = "..." ) fun update(status: StatusComponent.Model.LoadingStatus, amount: Int) diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/StatusesTileService.kt similarity index 92% rename from wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt rename to wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/StatusesTileService.kt index 9b8d8234..ff80dbe8 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/MainTileService.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/StatusesTileService.kt @@ -19,6 +19,7 @@ import com.makeevrserg.empireprojekt.mobile.wear.tile.components.MainTileRendere import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import ru.astrainteractive.klibs.kdi.Provider import ru.astrainteractive.klibs.kdi.Single import ru.astrainteractive.klibs.kdi.getValue @@ -28,12 +29,14 @@ private const val RESOURCES_VERSION = "1" * Skeleton for a tile with no images. */ @OptIn(ExperimentalHorologistApi::class) -class MainTileService : SuspendingTileService() { +class StatusesTileService : SuspendingTileService() { - private val wearRootModule by lazy { + private val wearRootModule by Provider { application.asEmpireApp().wearRootModule } - private val wearStatusComponent by wearRootModule.wearStatusComponent + private val wearStatusComponent by Provider { + wearRootModule.wearStatusComponent.value + } private val mainTileRenderer by Single { MainTileRenderer(applicationContext) } @@ -44,7 +47,7 @@ class MainTileService : SuspendingTileService() { lifecycleScope.launch { while (isActive) { delay(1000L) - TileService.getUpdater(applicationContext).requestUpdate(MainTileService::class.java) + TileService.getUpdater(applicationContext).requestUpdate(StatusesTileService::class.java) } } } diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/MainTileRenderer.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/MainTileRenderer.kt index 3bee3d2e..b3eed5e4 100644 --- a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/MainTileRenderer.kt +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/MainTileRenderer.kt @@ -58,6 +58,20 @@ class MainTileRenderer( ).build() ) .build() + + val textTimeStamp = Text.Builder(context, state.updatedAt) + .setTypography(Typography.TYPOGRAPHY_CAPTION1) + .setColor(theme.onSurface.asColorProp) + .setModifiers( + ModifiersBuilders.Modifiers.Builder() + .setClickable( + ModifiersBuilders.Clickable.Builder() + .setId("reload") + .setOnClick(ActionBuilders.LoadAction.Builder().build()) + .build() + ).build() + ) + .build() val compactChip = CompactChip.Builder( context, "More", @@ -82,12 +96,13 @@ class MainTileRenderer( .setWidth(DimensionBuilders.expand()) .addContent(image) .addContent(text) + .addContent(textTimeStamp) .addContent(statuses) - .addContent(compactChip) .build() return PrimaryLayout.Builder(buildDeviceParameters(context.resources)) .setContent(column) + .setPrimaryChipContent(compactChip) .build() } From 73ad0e7e9f265c2ef7093b762600a47f0a4755e9 Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Thu, 12 Oct 2023 00:02:53 +0300 Subject: [PATCH 19/20] fix delay --- .../empireprojekt/mobile/application/App.kt | 20 ++++++++++++++----- wearApp/src/main/AndroidManifest.xml | 4 ++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt index 023a371c..e606c88b 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/application/App.kt @@ -13,9 +13,13 @@ import com.makeevrserg.empireprojekt.mobile.features.root.di.impl.RootModuleImpl import com.makeevrserg.empireprojekt.mobile.wear.messenger.di.WearMessengerModule import com.makeevrserg.empireprojekt.mobile.work.CheckStatusWork import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import ru.astrainteractive.klibs.kdi.getValue import ru.astrainteractive.klibs.mikro.platform.DefaultAndroidPlatformConfiguration import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.seconds @OptIn(ExperimentalHorologistApi::class) class App : Application() { @@ -62,11 +66,17 @@ class App : Application() { TimeUnit.MINUTES ).build() val instanceWorkManager = WorkManager.getInstance(applicationContext) - instanceWorkManager.enqueueUniquePeriodicWork( - CheckStatusWork::class.java.simpleName, - ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, - statusWork - ) + + rootModule.servicesModule.mainScope.value.launch { + while (isActive) { + instanceWorkManager.enqueueUniquePeriodicWork( + CheckStatusWork::class.java.simpleName, + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + statusWork + ) + delay(30.seconds) + } + } } companion object { diff --git a/wearApp/src/main/AndroidManifest.xml b/wearApp/src/main/AndroidManifest.xml index b3d6285a..dba78156 100644 --- a/wearApp/src/main/AndroidManifest.xml +++ b/wearApp/src/main/AndroidManifest.xml @@ -31,10 +31,10 @@ android:value="0" /> From 33740b10a1618a40ae1a21440a4a431d0f668aba Mon Sep 17 00:00:00 2001 From: Roman Makeev Date: Thu, 12 Oct 2023 00:04:48 +0300 Subject: [PATCH 20/20] up version 0.1.0 6 --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index d5741d15..73369178 100644 --- a/gradle.properties +++ b/gradle.properties @@ -47,8 +47,8 @@ makeevrserg.android.sdk.target=34 # Project makeevrserg.project.name=EmpireProjektMobile makeevrserg.project.group=com.makeevrserg.empireprojekt.mobile -makeevrserg.project.version.string=0.0.3 -makeevrserg.project.version.code=5 +makeevrserg.project.version.string=0.1.0 +makeevrserg.project.version.code=6 makeevrserg.project.description=EmpireProjekt mobile application makeevrserg.project.developers=makeevrserg|Makeev Roman|makeevrserg@gmail.com makeevrserg.project.url=https://empireprojekt.ru