diff --git a/.gitignore b/.gitignore index a3a6e2ef..c2dd4aa1 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 @@ -38,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 5bedb857..826b84f9 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 @@ -32,9 +33,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 @@ -79,16 +86,14 @@ android { add("META-INF/LGPL2.1") } } - buildTypes { -// applicationVariants.all( -// com.makeevrserg.empireprojekt.mobile.ApplicationVariantAction( -// project -// ) -// ) + lint { + abortOnError = false } } dependencies { + // Kotlin + implementation(libs.kotlin.serialization.json) // Coroutines implementation(libs.kotlin.coroutines.core) implementation(libs.kotlin.coroutines.android) @@ -114,13 +119,22 @@ 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) 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") + // 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) 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/MainActivity.kt b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/MainActivity.kt index 4630191e..0e34161c 100644 --- a/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/MainActivity.kt +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/MainActivity.kt @@ -8,20 +8,20 @@ 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.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.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 import com.makeevrserg.empireprojekt.mobile.resources.R +import ru.astrainteractive.klibs.kdi.Provider import ru.astrainteractive.klibs.kdi.getValue @ExperimentalMaterialApi @@ -29,15 +29,16 @@ 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) 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 +52,7 @@ class MainActivity : ComponentActivity() { } } } - ComposeApplication(rootModule.themeSwitcher.value) { + ComposeApplication(rootModule.themeSwitcherComponent.value) { ModalBottomSheetLayout( sheetState = bottomSheetState.sheetState, sheetContent = bottomSheetState.sheetContent.value, @@ -60,7 +61,6 @@ class MainActivity : ComponentActivity() { ) { ApplicationContent( rootComponent = rootComponent, - rootBottomSheetComponent = rootBottomSheetComponent, modifier = Modifier ) } 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..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 @@ -1,22 +1,86 @@ 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 +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.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() { - private val servicesModule by RootModule.servicesModule + 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, + coroutineScope = rootModule.servicesModule.mainScope.value + ) + } + private val messageClient by lazy { + wearDataLayerRegistry.messageClient + } + + override fun onTerminate() { + super.onTerminate() + rootModule.servicesModule.mainScope.value.cancel() + } override fun onCreate() { super.onCreate() Firebase.initialize(this) - servicesModule.platformConfiguration.initialize( + rootModule.servicesModule.platformConfiguration.initialize { DefaultAndroidPlatformConfiguration( applicationContext ) - ) + } + scheduleWork() + } + + private fun scheduleWork() { + val statusWork = PeriodicWorkRequest.Builder( + CheckStatusWork::class.java, + 15, + TimeUnit.MINUTES + ).build() + val instanceWorkManager = WorkManager.getInstance(applicationContext) + + rootModule.servicesModule.mainScope.value.launch { + while (isActive) { + instanceWorkManager.enqueueUniquePeriodicWork( + CheckStatusWork::class.java.simpleName, + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + statusWork + ) + delay(30.seconds) + } + } + } + + 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 new file mode 100644 index 00000000..5a6bcb3e --- /dev/null +++ b/androidApp/src/main/java/com/makeevrserg/empireprojekt/mobile/work/CheckStatusWork.kt @@ -0,0 +1,61 @@ +package com.makeevrserg.empireprojekt.mobile.work + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +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.model.StatusModel +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import ru.astrainteractive.klibs.kdi.Provider +import ru.astrainteractive.klibs.kdi.getValue + +class CheckStatusWork( + context: Context, + params: WorkerParameters +) : CoroutineWorker(context, params) { + private val wearMessengerModule by lazy { + applicationContext.asEmpireApp().wearMessengerModule + } + private val rootModule by lazy { + applicationContext.asEmpireApp().rootModule + } + private val rootStatusComponent by Provider { + rootModule.rootStatusComponent.value + } + + override suspend fun doWork(): Result { + Log.d(TAG, "doWork: ") + 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() + wearMessengerModule.wearMessageProducer.produce( + message = wearMessengerModule.statusModelMessage, + value = messages + ) + } + + companion object { + private const val TAG = "CheckStatusWork" + } +} 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 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 68d720e0..8b901272 100644 Binary files a/iosApp/iosApp.xcworkspace/xcuserdata/romanmakeev.xcuserdatad/UserInterfaceState.xcuserstate and b/iosApp/iosApp.xcworkspace/xcuserdata/romanmakeev.xcuserdatad/UserInterfaceState.xcuserstate differ 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/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 a8fabc5f..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,143 +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.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.theme.ThemeSwitcher -import com.makeevrserg.empireprojekt.mobile.services.core.CoroutineFeature +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 -> { - 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 - ) - ) - } - } - } + 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 statusComponents: List, - 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 b7abd4f6..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,20 +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.theme.ThemeSwitcher -import com.russhwolf.settings.Settings -import kotlinx.coroutines.CoroutineScope +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 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 settings: Single - val dispatchers: Single - val mainScope: Single - val themeSwitcher: Single + val servicesModule: ServicesModule + val statusModule: StatusModule + val splashModule: SplashComponentModule - companion object : RootModule by RootModuleImpl + // Global components + val rootStatusComponent: Single + 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/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/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..4f488cf3 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/RootModuleImpl.kt @@ -0,0 +1,44 @@ +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 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 + +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 { + val module = ThemeSwitcherModule.Default(servicesModule.settings.value) + DefaultThemeSwitcherComponentComponent(module) + } +} 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 68% 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 91961cd9..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.factories.LinkBrowserFactory +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 8ed66afe..00000000 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/root/RootModuleImpl.kt +++ /dev/null @@ -1,33 +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.factories.SettingsFactory -import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher -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 themeSwitcher: Single = Single { - ThemeSwitcherComponent(settings.value) - } -} 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 60535685..00000000 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/di/impl/splash/SplashComponentModuleImpl.kt +++ /dev/null @@ -1,23 +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.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 { - SplashComponentRepository.Default() - }.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/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..965c2fb2 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/root/screen/di/factory/RootScreenComponentChildFactory.kt @@ -0,0 +1,32 @@ +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.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 = rootModule.splashModule + ) + ) + + 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/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 new file mode 100644 index 00000000..84831968 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/StubStatusComponent.kt @@ -0,0 +1,52 @@ +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 { + 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() + ) + } + } + + 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/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/status/DefaultMinecraftStatusComponent.kt b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/mincraft/DefaultMinecraftStatusComponent.kt similarity index 87% 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..1b72a31b 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,6 @@ -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 @@ -14,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 @@ -39,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) @@ -68,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/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..5f405c06 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/status/root/DefaultRootStatusComponent.kt @@ -0,0 +1,48 @@ +package com.makeevrserg.empireprojekt.mobile.features.status.root + +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( + private val statusModule: StatusModule +) : RootStatusComponent { + override val statusComponents: List = buildList { + + DefaultUrlStatusComponent( + url = "https://empireprojekt.ru", + title = "empireprojekt.ru", + module = statusModule, + coroutineFeature = CoroutineFeature.Default() + ).run(::add) + + DefaultUrlStatusComponent( + url = "https://astrainteractive.ru", + title = "astrainteractive.ru", + module = statusModule, + coroutineFeature = CoroutineFeature.Default() + ).run(::add) + + DefaultUrlStatusComponent( + url = "http://astralearner.empireprojekt.ru:8083/dictionaries/4/words", + title = "Dev: AstraLearner", + module = statusModule, + coroutineFeature = CoroutineFeature.Default() + ).run(::add) + + DefaultUrlStatusComponent( + url = "http://astralearner.empireprojekt.ru:8081/dictionaries/4/words", + title = "Prod: AstraLearner", + module = statusModule, + coroutineFeature = CoroutineFeature.Default() + ).run(::add) + + DefaultMinecraftStatusComponent( + title = "Empire SMP", + module = statusModule, + coroutineFeature = 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 79% 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..a43314d6 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,6 @@ -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 +14,20 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class UrlStatusComponent( - context: ComponentContext, +class DefaultUrlStatusComponent( url: String, title: String, module: StatusModule, private val coroutineFeature: CoroutineFeature -) : StatusComponent, StatusModule by module, ComponentContext by context { +) : UrlStatusComponent, StatusModule by module { 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 @@ -47,7 +40,7 @@ class UrlStatusComponent( } } - private suspend fun checkOnce(force: Boolean) { + override suspend fun checkOnce(force: Boolean) { if (_model.value.isLoading && !force) return _model.update { it.copy(isLoading = true) @@ -82,6 +75,6 @@ class UrlStatusComponent( } 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/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/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..b20e16fb --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/DefaultThemeSwitcherComponentComponent.kt @@ -0,0 +1,40 @@ +package com.makeevrserg.empireprojekt.mobile.features.theme + +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.kdi.Provider +import ru.astrainteractive.klibs.kdi.getValue +import ru.astrainteractive.klibs.mikro.core.util.next + +class DefaultThemeSwitcherComponentComponent( + themeSwitcherModule: ThemeSwitcherModule +) : ThemeSwitcherComponent, ThemeSwitcherModule by themeSwitcherModule { + private val themeFlowStorageValue by Provider { + themeSwitcherRepository.themeFlowStorageValue + } + + override val theme: StateFlow = themeFlowStorageValue.stateFlow + + override fun selectDarkTheme() { + themeFlowStorageValue.save(Theme.DARK) + } + + override fun selectLightTheme() { + themeFlowStorageValue.save(Theme.LIGHT) + } + + override fun selectTheme(theme: Theme) { + themeFlowStorageValue.save(theme) + } + + override fun next() { + val entries = Theme.entries.toTypedArray() + val nextTheme = theme.value.next(entries) + selectTheme(nextTheme) + } + + init { + themeFlowStorageValue.load() + } +} 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..90764a65 --- /dev/null +++ b/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/PreviewThemeSwitcherComponent.kt @@ -0,0 +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(Theme.LIGHT) + + override fun selectDarkTheme() { + selectTheme(Theme.DARK) + } + + override fun selectLightTheme() { + selectTheme(Theme.LIGHT) + } + + override fun selectTheme(theme: Theme) { + this.theme.value = theme + } + + override fun next() { + theme.value.next(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 fdb4f1ea..00000000 --- a/modules/features/root/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/features/theme/ThemeSwitcher.kt +++ /dev/null @@ -1,15 +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) -} 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..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,37 +1,13 @@ package com.makeevrserg.empireprojekt.mobile.features.theme -import com.russhwolf.settings.Settings +import com.makeevrserg.empireprojekt.mobile.features.theme.data.model.Theme import kotlinx.coroutines.flow.StateFlow -import ru.astrainteractive.klibs.kstorage.StateFlowMutableStorageValue -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) - } - - override fun selectLightTheme() { - themeFlowStorageValue.save(ThemeSwitcher.Theme.LIGHT) - } - - override fun selectTheme(theme: ThemeSwitcher.Theme) { - themeFlowStorageValue.save(theme) - } - - init { - themeFlowStorageValue.load() - } + 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/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/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 77% 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 a4ec0579..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,5 +1,6 @@ -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 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/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 92% 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..7aeb6145 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, @@ -17,7 +17,7 @@ class SplashComponentImpl( 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/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/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 8c9d4e9b..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 @@ -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 9464841e..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,16 +26,15 @@ 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, - statusComponents = screen.statusComponents + 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 bb963f6e..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 @@ -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,26 @@ 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.ThemeSwitcher +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 -private fun ThemeSwitcher.Theme.toComposeTheme() = when (this) { - ThemeSwitcher.Theme.DARK -> AppTheme.DefaultDarkTheme - ThemeSwitcher.Theme.LIGHT -> AppTheme.DefaultLightTheme +fun Theme.toComposeTheme() = when (this) { + Theme.DARK -> AppTheme.DefaultDarkTheme + Theme.LIGHT -> AppTheme.DefaultLightTheme } -@OptIn(ExperimentalAnimationApi::class) @Composable -fun ComposeApplication(themeSwitcher: ThemeSwitcher, 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) - AnimatedContent( + Crossfade( targetState = appTheme, - transitionSpec = { fadeIn() with fadeOut() } ) { appTheme -> CompositionLocalProvider( LocalAppTheme provides appTheme, 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 bc2bd5c7..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 @@ -18,20 +18,20 @@ 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.status.StatusComponent -import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcher +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 @Composable fun StatusScreen( - rootComponent: DefaultRootComponent, - rootBottomSheetComponent: RootBottomSheetComponent, - themeSwitcher: ThemeSwitcher, - statusComponents: List, + rootComponent: RootComponent, + themeSwitcherComponent: ThemeSwitcherComponent, + rootStatusComponent: RootStatusComponent, ) { Scaffold( modifier = Modifier, @@ -40,7 +40,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 +65,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( + Theme.values() + ) + themeSwitcherComponent.selectTheme(nextTheme) } ) } @@ -85,7 +86,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 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/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 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..99b364e6 --- /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 = "/statuses" +) 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/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..98025055 --- /dev/null +++ b/modules/services/wear-messenger/src/commonMain/kotlin/com/makeevrserg/empireprojekt/mobile/wear/messenger/di/WearMessengerModule.kt @@ -0,0 +1,57 @@ +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 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 + + 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 + ) + } + + @OptIn(ExperimentalHorologistApi::class) + override val wearMessageReceiver: WearMessageReceiver by lazy { + WearMessageReceiverImpl( + wearDataLayerRegistry = wearDataLayerRegistry, + messageClient = messageClient + ) + } + override val statusModelMessage: StatusModelMessage = StatusModelMessage( + json = json + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 021ff1d5..5164e99e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,9 +23,11 @@ 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") +include(":modules:services:wear-messenger") // Feature include(":modules:features:root") include(":modules:features:splash") diff --git a/wearApp/build.gradle.kts b/wearApp/build.gradle.kts new file mode 100644 index 00000000..3a7b139e --- /dev/null +++ b/wearApp/build.gradle.kts @@ -0,0 +1,133 @@ +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 { + // Kotlin + implementation(libs.kotlin.serialization.json) + // 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.wear.compose:compose-navigation:1.2.0") + + 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") + 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) + implementation(libs.klibs.kstorage) + implementation(libs.klibs.kdi) + // Settings + implementation(libs.mppsettings) + // moko + implementation(libs.moko.resources.core) + // 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) + implementation(projects.modules.services.wearMessenger) +} diff --git a/wearApp/src/main/AndroidManifest.xml b/wearApp/src/main/AndroidManifest.xml new file mode 100644 index 00000000..dba78156 --- /dev/null +++ b/wearApp/src/main/AndroidManifest.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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..63c61de7 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/MainActivity.kt @@ -0,0 +1,35 @@ +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 androidx.wear.compose.navigation.rememberSwipeDismissableNavController +import com.makeevrserg.empireprojekt.mobile.features.ui.root.ComposeApplication +import com.makeevrserg.empireprojekt.mobile.wear.application.App.Companion.asEmpireApp +import com.makeevrserg.empireprojekt.mobile.wear.features.root.NavHostRootComponent +import com.makeevrserg.empireprojekt.mobile.wear.features.root.RootScreen +import ru.astrainteractive.klibs.kdi.Provider +import ru.astrainteractive.klibs.kdi.getValue + +class MainActivity : ComponentActivity() { + private val rootModule by Provider { + application.asEmpireApp().wearRootModule + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + setTheme(com.makeevrserg.empireprojekt.mobile.resources.R.style.AppTheme) + setContent { + val navController = rememberSwipeDismissableNavController() + val navHostRootComponent = NavHostRootComponent(navController) + ComposeApplication(rootModule.themeSwitcherComponent.value) { + RootScreen( + rootComponent = navHostRootComponent, + wearRootModule = rootModule + ) + } + } + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/application/App.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/application/App.kt new file mode 100644 index 00000000..937f7403 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/application/App.kt @@ -0,0 +1,25 @@ +package com.makeevrserg.empireprojekt.mobile.wear.application + +import android.app.Application +import android.content.Context +import com.makeevrserg.empireprojekt.mobile.wear.di.impl.WearRootModuleImpl +import ru.astrainteractive.klibs.mikro.platform.DefaultAndroidPlatformConfiguration + +class App : Application() { + val wearRootModule by lazy { + WearRootModuleImpl() + } + + override fun onCreate() { + super.onCreate() + wearRootModule.platformConfiguration.initialize { + DefaultAndroidPlatformConfiguration( + applicationContext + ) + } + } + companion object { + fun Application.asEmpireApp(): App = (this as App) + fun Context.asEmpireApp(): App = (applicationContext as App) + } +} 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..a8f25f48 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/WearRootModule.kt @@ -0,0 +1,20 @@ +package com.makeevrserg.empireprojekt.mobile.wear.di + +import com.makeevrserg.empireprojekt.mobile.features.theme.ThemeSwitcherComponent +import com.makeevrserg.empireprojekt.mobile.wear.features.status.WearStatusComponent +import com.makeevrserg.empireprojekt.mobile.wear.messenger.di.WearMessengerModule +import com.russhwolf.settings.Settings +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.platform.PlatformConfiguration + +interface WearRootModule : Module { + val platformConfiguration: Lateinit + val settings: Single + val jsonConfiguration: Single + val themeSwitcherComponent: Single + val wearStatusComponent: Single + 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 new file mode 100644 index 00000000..abf11495 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/di/impl/WearRootModuleImpl.kt @@ -0,0 +1,57 @@ +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 + +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 { + 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 { + DefaultWearStatusComponent( + wearMessageReceiver = wearMessengerModule.wearMessageReceiver, + coroutineScope = mainScope + ) + } +} 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..3ec76335 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/components/AstraChip.kt @@ -0,0 +1,78 @@ +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 +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) + ), + ) +} + +@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/main/MainScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt new file mode 100644 index 00000000..3fe3a7e2 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/MainScreen.kt @@ -0,0 +1,55 @@ +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 com.makeevrserg.empireprojekt.mobile.wear.features.root.NavHostRootComponent +import ru.astrainteractive.klibs.kdi.getValue + +@Composable +fun MainScreen( + wearRootModule: WearRootModule, + rootComponent: NavHostRootComponent +) { + val themeSwitcher by wearRootModule.themeSwitcherComponent + 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(themeSwitcherComponent = themeSwitcher) + Spacer(modifier = Modifier.height(AppTheme.dimens.S)) + 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 new file mode 100644 index 00000000..b011235b --- /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, onClick: () -> Unit) { + AstraChip( + modifier = Modifier.fillMaxWidth(), + label = { + Text( + text = text, + style = AppTheme.typography.caption, + color = AppTheme.materialColor.onPrimary + ) + }, + onClick = 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..e9710993 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/components/ThemeChip.kt @@ -0,0 +1,61 @@ +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.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) { + Theme.DARK -> Icons.Filled.Bedtime + Theme.LIGHT -> Icons.Filled.WbSunny + } + val color by animateColorAsState( + targetValue = when (theme) { + Theme.DARK -> AppTheme.materialColor.onPrimary + 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 = themeSwitcherComponent::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..425b6c5e --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/main/preview/RootScreenPreview.kt @@ -0,0 +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.impl.WearRootModuleImpl +import com.makeevrserg.empireprojekt.mobile.wear.features.main.MainScreen +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 + ) + } +} diff --git a/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/NavHostRootComponent.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/NavHostRootComponent.kt new file mode 100644 index 00000000..fc1416f4 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/NavHostRootComponent.kt @@ -0,0 +1,10 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.root + +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/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/root/RootScreen.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/RootScreen.kt new file mode 100644 index 00000000..16e00b01 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/root/RootScreen.kt @@ -0,0 +1,26 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.root + +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 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) + } + composable(RootComponent.Child.Statuses::class.simpleName!!) { + 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..7bf5794e --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/DefaultWearStatusComponent.kt @@ -0,0 +1,46 @@ +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 +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +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 }, + 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 new file mode 100644 index 00000000..bd7977cb --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/StatusesScreen.kt @@ -0,0 +1,81 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.status + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.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 +import com.makeevrserg.empireprojekt.mobile.wear.features.components.IconTextChip + +@Composable +fun StatusesScreen(wearStatusComponent: WearStatusComponent) { + val mergedState by wearStatusComponent.mergedState.collectAsState() + 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), + ) { + item { + Text( + text = mergedState.updatedAt, + style = AppTheme.typography.caption, + color = AppTheme.materialColor.onPrimary, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + item { + IconTextChip( + modifier = Modifier.fillMaxWidth(), + text = mergedState.successCount.toString(), + imageVector = Icons.Filled.WifiTethering, + iconColor = AppTheme.alColors.colorPositive + ) + } + item { + IconTextChip( + modifier = Modifier.fillMaxWidth(), + text = mergedState.loadingCount.toString(), + imageVector = Icons.Filled.WifiTetheringError, + iconColor = AppTheme.alColors.astraOrange + ) + } + item { + IconTextChip( + modifier = Modifier.fillMaxWidth(), + text = mergedState.failureCount.toString(), + imageVector = Icons.Filled.WifiTetheringOff, + iconColor = AppTheme.alColors.colorNegative + ) + } + } + } +} 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..7c5a7df4 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/WearStatusComponent.kt @@ -0,0 +1,32 @@ +package com.makeevrserg.empireprojekt.mobile.wear.features.status + +import com.makeevrserg.empireprojekt.mobile.features.status.StatusComponent +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +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 updatedAt: String = "..." + ) + + fun update(status: StatusComponent.Model.LoadingStatus, amount: Int) + + class Stub : WearStatusComponent { + 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) + } + } + } + } +} 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..856d7273 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/features/status/preview/StatusesScreenPreview.kt @@ -0,0 +1,15 @@ +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.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() { + ComposeApplication { + StatusesScreen(WearStatusComponent.Stub()) + } +} 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..4391bea9 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/service/DataLayerListenerService.kt @@ -0,0 +1,49 @@ +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.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) } + } + + 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() { + 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" + } +} 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..355d65e2 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/ComposeTileService.kt @@ -0,0 +1,103 @@ +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.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.glance.wear.tiles.action.ActionCallback +import com.makeevrserg.empireprojekt.mobile.resources.R +import com.makeevrserg.empireprojekt.mobile.wear.MainActivity +import java.time.Instant + +/** + * 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) + ) + ) + ) + + object EmptyAction : ActionCallback { + override suspend fun onAction( + context: Context, + glanceId: GlanceId + ) = Unit + } + + @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)) + 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/StatusesTileService.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/StatusesTileService.kt new file mode 100644 index 00000000..ff80dbe8 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/StatusesTileService.kt @@ -0,0 +1,101 @@ +package com.makeevrserg.empireprojekt.mobile.wear.tile + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.lifecycle.lifecycleScope +import androidx.wear.protolayout.ColorBuilders +import androidx.wear.protolayout.ColorBuilders.ColorProp +import androidx.wear.protolayout.LayoutElementBuilders +import androidx.wear.protolayout.ResourceBuilders +import androidx.wear.protolayout.TimelineBuilders +import androidx.wear.tiles.RequestBuilders +import androidx.wear.tiles.TileBuilders +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.application.App.Companion.asEmpireApp +import com.makeevrserg.empireprojekt.mobile.wear.tile.components.MainTileRenderer +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 + +private const val RESOURCES_VERSION = "1" + +/** + * Skeleton for a tile with no images. + */ +@OptIn(ExperimentalHorologistApi::class) +class StatusesTileService : SuspendingTileService() { + + private val wearRootModule by Provider { + application.asEmpireApp().wearRootModule + } + private val wearStatusComponent by Provider { + wearRootModule.wearStatusComponent.value + } + private val mainTileRenderer by Single { + MainTileRenderer(applicationContext) + } + + override fun onCreate() { + super.onCreate() + + lifecycleScope.launch { + while (isActive) { + delay(1000L) + TileService.getUpdater(applicationContext).requestUpdate(StatusesTileService::class.java) + } + } + } + + 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 state = wearStatusComponent.mergedState.value + val layout = LayoutElementBuilders.Layout.Builder() + .setRoot(mainTileRenderer.renderTile(state, requestParams.deviceConfiguration)) + .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() + +val Int.asColorProp: ColorBuilders.ColorProp + get() = ColorProp.Builder() + .setArgb(this) + .build() 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/MainTileRenderer.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/MainTileRenderer.kt new file mode 100644 index 00000000..b3eed5e4 --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/MainTileRenderer.kt @@ -0,0 +1,124 @@ +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 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", + 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(textTimeStamp) + .addContent(statuses) + .build() + + return PrimaryLayout.Builder(buildDeviceParameters(context.resources)) + .setContent(column) + .setPrimaryChipContent(compactChip) + .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/StatusesRowRenderer.kt b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowRenderer.kt new file mode 100644 index 00000000..c915b06d --- /dev/null +++ b/wearApp/src/main/java/com/makeevrserg/empireprojekt/mobile/wear/tile/components/StatusesRowRenderer.kt @@ -0,0 +1,78 @@ +package com.makeevrserg.empireprojekt.mobile.wear.tile.components + +import android.content.Context +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +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.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 java.util.UUID + +@OptIn(ExperimentalHorologistApi::class) +class StatusesRowRenderer( + context: Context, +) : SingleTileLayoutRenderer(context) { + + private fun statusButton( + amount: Int, + accentColor: Color, + ): LayoutElementBuilders.LayoutElement { + return Button.Builder( + context, + ModifiersBuilders.Clickable.Builder() + .setId(UUID.randomUUID().toString()) + .setOnClick(ActionBuilders.LoadAction.Builder().build()) + .build() + ).setButtonColors( + ButtonColors( + theme.surface, + accentColor.toArgb() + ) + ).setTextContent(amount.toString()).build() + } + + 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( + amount = state.successCount, + accentColor = AppTheme.DefaultDarkTheme.alColors.colorPositive, + ) + ) + .addContent( + LayoutElementBuilders.Box.Builder().setWidth(DimensionBuilders.dp(8f)).setHeight( + DimensionBuilders.expand() + ).build() + ) + .addContent( + statusButton( + amount = state.loadingCount, + accentColor = AppTheme.DefaultDarkTheme.alColors.astraOrange, + ) + ) + .addContent( + LayoutElementBuilders.Box.Builder().setWidth(DimensionBuilders.dp(8f)).setHeight( + DimensionBuilders.expand() + ).build() + ) + .addContent( + statusButton( + amount = state.failureCount, + accentColor = AppTheme.DefaultDarkTheme.alColors.colorNegative, + ) + ).build() + } +}