diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b589d56
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/copyright.xml b/.idea/copyright/copyright.xml
new file mode 100644
index 0000000..a2bf397
--- /dev/null
+++ b/.idea/copyright/copyright.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..91283a0
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
new file mode 100644
index 0000000..341d6f9
--- /dev/null
+++ b/.idea/deploymentTargetDropDown.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..73e44de
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..103e00c
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..0bc90b0
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..69e8615
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/ktlint.xml b/.idea/ktlint.xml
new file mode 100644
index 0000000..a001d0e
--- /dev/null
+++ b/.idea/ktlint.xml
@@ -0,0 +1,12 @@
+
+
+
+ false
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..8978d23
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..438cf46
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-parcelize")
+ kotlin("kapt")
+}
+
+repositories {
+ mavenCentral()
+ google()
+}
+
+android {
+ val localProperties = gradleLocalProperties(rootDir)
+
+ namespace = Config.applicationId
+ compileSdk = Config.compileSdkVersion
+
+ defaultConfig {
+ applicationId = Config.applicationId
+ minSdk = Config.minSdkVersion
+ targetSdk = Config.targetSdkVersion
+ versionCode = Config.versionCode
+ versionName = Config.versionName
+
+ testInstrumentationRunner = Config.testInstrumentRunner
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ signingConfigs {
+ create("release") {
+ storeFile = file(localProperties.getProperty("storeFile"))
+ storePassword = localProperties.getProperty("storePassword")
+ keyAlias = localProperties.getProperty("keyAlias")
+ keyPassword = localProperties.getProperty("keyPassword")
+ }
+ getByName("debug") {
+ storeFile = file(localProperties.getProperty("storeFile"))
+ storePassword = localProperties.getProperty("storePassword")
+ keyAlias = localProperties.getProperty("keyAlias")
+ keyPassword = localProperties.getProperty("keyPassword")
+ }
+ }
+
+ buildTypes {
+ getByName("release") {
+ isDebuggable = false
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+ signingConfig = signingConfigs.getByName("release")
+ }
+ getByName("debug") {
+ applicationIdSuffix = ".debug"
+ isDebuggable = true
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = Config.jvmTarget
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Config.kotlinCompiler
+ }
+
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+ implementation(project(":module_injector"))
+ implementation(project(":core:common"))
+ implementation(project(":core:ui"))
+ implementation(project(":features:home:impl"))
+ implementation(project(":features:home:api"))
+ implementation(project(":features:settings:impl"))
+ implementation(project(":features:settings:api"))
+ implementation(project(":features:player:impl"))
+ implementation(project(":features:player:api"))
+
+ implementation(Dependencies.AndroidX.core)
+ implementation(Dependencies.AndroidX.appcompat)
+ implementation(Dependencies.AndroidX.material)
+ implementation(Dependencies.AndroidX.googleMaterial)
+ implementation(Dependencies.AndroidX.lifecycleRuntime)
+ implementation(Dependencies.AndroidX.systemUiController)
+
+ implementation(Dependencies.Compose.ui)
+ implementation(Dependencies.Compose.activity)
+
+ implementation(Dependencies.Dagger.core)
+ kapt(Dependencies.Dagger.kapt)
+
+ implementation(Dependencies.Room.core)
+ kapt(Dependencies.Room.kapt)
+
+ implementation(Dependencies.Voyager.navigator)
+ implementation(Dependencies.Voyager.screenModel)
+ implementation(Dependencies.Voyager.transitions)
+
+ testImplementation(Dependencies.Test.jUnit)
+ androidTestImplementation(Dependencies.Test.jUnitExt)
+ androidTestImplementation(Dependencies.Test.espresso)
+ androidTestImplementation(Dependencies.Test.composeJUnit)
+ debugImplementation(Dependencies.Compose.uiTooling)
+ debugImplementation(Dependencies.Compose.uiTestManifest)
+
+ debugImplementation(Dependencies.Leakcanary.library)
+}
diff --git a/app/debug/app-debug.apk b/app/debug/app-debug.apk
new file mode 100644
index 0000000..62dff2d
Binary files /dev/null and b/app/debug/app-debug.apk differ
diff --git a/app/debug/output-metadata.json b/app/debug/output-metadata.json
new file mode 100644
index 0000000..b263979
--- /dev/null
+++ b/app/debug/output-metadata.json
@@ -0,0 +1,20 @@
+{
+ "version": 3,
+ "artifactType": {
+ "type": "APK",
+ "kind": "Directory"
+ },
+ "applicationId": "ru.aleshin.mixplayer.debug",
+ "variantName": "debug",
+ "elements": [
+ {
+ "type": "SINGLE",
+ "filters": [],
+ "attributes": [],
+ "versionCode": 1,
+ "versionName": "0.1-alpha",
+ "outputFile": "app-debug.apk"
+ }
+ ],
+ "elementType": "File"
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/release/app-release.apk b/app/release/app-release.apk
new file mode 100644
index 0000000..1745402
Binary files /dev/null and b/app/release/app-release.apk differ
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
new file mode 100644
index 0000000..f907af4
--- /dev/null
+++ b/app/release/output-metadata.json
@@ -0,0 +1,20 @@
+{
+ "version": 3,
+ "artifactType": {
+ "type": "APK",
+ "kind": "Directory"
+ },
+ "applicationId": "ru.aleshin.mixplayer",
+ "variantName": "release",
+ "elements": [
+ {
+ "type": "SINGLE",
+ "filters": [],
+ "attributes": [],
+ "versionCode": 1,
+ "versionName": "0.1-alpha",
+ "outputFile": "app-release.apk"
+ }
+ ],
+ "elementType": "File"
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/ru/aleshin/mixplayer/ExampleInstrumentedTest.kt b/app/src/androidTest/java/ru/aleshin/mixplayer/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..6bd80f5
--- /dev/null
+++ b/app/src/androidTest/java/ru/aleshin/mixplayer/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package ru.aleshin.mixplayer
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.example.mixplayer", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..79f531a
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/database/mixplayer_prepopulated_settings.db b/app/src/main/assets/database/mixplayer_prepopulated_settings.db
new file mode 100644
index 0000000..a40b13c
Binary files /dev/null and b/app/src/main/assets/database/mixplayer_prepopulated_settings.db differ
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..f04300d
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/ru/aleshin/mixplayer/application/MixPlayerApp.kt b/app/src/main/java/ru/aleshin/mixplayer/application/MixPlayerApp.kt
new file mode 100644
index 0000000..3d08a10
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/application/MixPlayerApp.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.application
+
+import android.app.Application
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import ru.aleshin.core.common.functional.Constants
+import ru.aleshin.core.common.notifications.parameters.NotificationPriority
+import ru.aleshin.features.player.api.presentation.player.PlayerService
+import ru.aleshin.mixplayer.di.component.AppComponent
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+class MixPlayerApp : Application() {
+
+ val appComponent by lazy {
+ AppComponent.create(applicationContext)
+ }
+
+ val serviceIntent by lazy {
+ Intent(this, PlayerService::class.java)
+ }
+
+ private val notificationCreator by lazy {
+ appComponent.fetchNotificationCreator()
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createNotificationChannel()
+ startForegroundService(serviceIntent)
+ } else {
+ startService(serviceIntent)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun createNotificationChannel() = notificationCreator.createNotifyChannel(
+ channelId = Constants.Notification.CHANNEL_ID,
+ channelName = Constants.Notification.CHANNEL_NAME,
+ priority = NotificationPriority.LOW,
+ )
+}
+
+fun Context.fetchApp(): MixPlayerApp {
+ return applicationContext as MixPlayerApp
+}
+
+@Composable
+fun fetchAppComponent(): AppComponent {
+ return LocalContext.current.fetchApp().appComponent
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/di/component/AppComponent.kt b/app/src/main/java/ru/aleshin/mixplayer/di/component/AppComponent.kt
new file mode 100644
index 0000000..62fbd0d
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/di/component/AppComponent.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.di.component
+
+import android.content.Context
+import dagger.BindsInstance
+import dagger.Component
+import ru.aleshin.core.common.notifications.NotificationCreator
+import ru.aleshin.mixplayer.presentation.ui.main.MainActivity
+import ru.aleshin.features.home.impl.di.HomeFeatureDependencies
+import ru.aleshin.features.player.impl.di.PlayerFeatureDependencies
+import ru.aleshin.features.settings.impl.di.SettingsFeatureDependencies
+import ru.aleshin.mixplayer.di.modules.CoreModule
+import ru.aleshin.mixplayer.di.modules.DataBaseModule
+import ru.aleshin.mixplayer.di.modules.DataModule
+import ru.aleshin.mixplayer.di.modules.DependenciesModule
+import ru.aleshin.mixplayer.di.modules.DomainModules
+import ru.aleshin.mixplayer.di.modules.FeatureModule
+import ru.aleshin.mixplayer.di.modules.NavigationModule
+import ru.aleshin.mixplayer.di.modules.PresentationModule
+import javax.inject.Singleton
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Singleton
+@Component(
+ modules = [
+ DataBaseModule::class,
+ DataModule::class,
+ NavigationModule::class,
+ CoreModule::class,
+ PresentationModule::class,
+ DomainModules::class,
+ DependenciesModule::class,
+ FeatureModule::class,
+ ],
+)
+interface AppComponent : AppDependencies {
+
+ fun inject(activity: MainActivity)
+ fun fetchNotificationCreator(): NotificationCreator
+
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun applicationContext(context: Context): Builder
+ fun dataBaseModule(module: DataBaseModule): Builder
+ fun navigationModule(module: NavigationModule): Builder
+ fun featureModule(module: FeatureModule): Builder
+ fun build(): AppComponent
+ }
+
+ companion object {
+ fun create(context: Context): AppComponent {
+ return DaggerAppComponent.builder()
+ .applicationContext(context)
+ .dataBaseModule(DataBaseModule())
+ .navigationModule(NavigationModule())
+ .featureModule(FeatureModule())
+ .build()
+ }
+ }
+}
+
+interface AppDependencies :
+ HomeFeatureDependencies,
+ PlayerFeatureDependencies,
+ SettingsFeatureDependencies
diff --git a/app/src/main/java/ru/aleshin/mixplayer/di/modules/CoreModule.kt b/app/src/main/java/ru/aleshin/mixplayer/di/modules/CoreModule.kt
new file mode 100644
index 0000000..8e71a2e
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/di/modules/CoreModule.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.mixplayer.di.modules
+
+import dagger.Binds
+import dagger.Module
+import ru.aleshin.core.common.managers.CoroutineManager
+import ru.aleshin.core.common.managers.DateManager
+import ru.aleshin.core.common.notifications.NotificationCreator
+import javax.inject.Singleton
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Module
+interface CoreModule {
+
+ @Binds
+ fun bindNotificationCreator(creator: NotificationCreator.Base): NotificationCreator
+
+ @Binds
+ @Singleton
+ fun bindCoroutineManager(manager: CoroutineManager.Base): CoroutineManager
+
+ @Binds
+ fun bindDateManager(manager: DateManager.Base): DateManager
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/di/modules/DataBaseModule.kt b/app/src/main/java/ru/aleshin/mixplayer/di/modules/DataBaseModule.kt
new file mode 100644
index 0000000..b640167
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/di/modules/DataBaseModule.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.di.modules
+
+import android.content.Context
+import android.media.MediaMetadataRetriever
+import androidx.room.Room
+import dagger.Module
+import dagger.Provides
+import ru.aleshin.features.settings.api.data.datasource.SettingsDao
+import ru.aleshin.features.settings.api.data.datasource.SettingsDataBase
+import javax.inject.Singleton
+
+/**
+ * @author Stanislav Aleshin on 15.06.2023.
+ */
+@Module
+class DataBaseModule {
+
+ @Provides
+ @Singleton
+ fun provideSettingsDataBase(applicationContext: Context): SettingsDataBase {
+ return Room.databaseBuilder(
+ context = applicationContext,
+ klass = SettingsDataBase::class.java,
+ name = SettingsDataBase.NAME,
+ ).createFromAsset("database/mixplayer_prepopulated_settings.db").build()
+ }
+
+ @Provides
+ @Singleton
+ fun provideSettingsDao(dataBase: SettingsDataBase): SettingsDao {
+ return dataBase.fetchSettingsDao()
+ }
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/di/modules/DataModule.kt b/app/src/main/java/ru/aleshin/mixplayer/di/modules/DataModule.kt
new file mode 100644
index 0000000..73d1fad
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/di/modules/DataModule.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.mixplayer.di.modules
+
+import dagger.Binds
+import dagger.Module
+import ru.aleshin.features.home.api.data.datasources.AppAudioLocalDataSource
+import ru.aleshin.features.home.api.data.datasources.AudioStoreManager
+import ru.aleshin.features.home.api.data.datasources.MediaQueryParser
+import ru.aleshin.features.home.api.data.datasources.SystemAudioLocalDataSource
+import ru.aleshin.features.home.api.data.datasources.VideosLocalDataSource
+import ru.aleshin.features.home.api.data.datasources.VideosStoreManager
+import ru.aleshin.features.home.api.data.repository.AppAudioRepositoryImpl
+import ru.aleshin.features.home.api.data.repository.SystemAudioRepositoryImpl
+import ru.aleshin.features.home.api.data.repository.VideosRepositoryImpl
+import ru.aleshin.features.home.api.domain.repositories.AppAudioRepository
+import ru.aleshin.features.home.api.domain.repositories.SystemAudioRepository
+import ru.aleshin.features.home.api.domain.repositories.VideosRepository
+import ru.aleshin.features.settings.api.data.datasource.SettingsLocalDataSource
+import ru.aleshin.features.settings.api.data.repositories.SettingsRepositoryImpl
+import ru.aleshin.features.settings.api.domain.repositories.SettingsRepository
+import javax.inject.Singleton
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Module
+interface DataModule {
+
+ @Binds
+ @Singleton
+ fun bindSettingsLocalDataSource(dataSource: SettingsLocalDataSource.Base): SettingsLocalDataSource
+
+ @Binds
+ @Singleton
+ fun bindSettingsRepository(repository: SettingsRepositoryImpl): SettingsRepository
+
+ @Binds
+ @Singleton
+ fun bindSystemAudioLocalDataSource(dataSource: SystemAudioLocalDataSource.Base): SystemAudioLocalDataSource
+
+ @Binds
+ @Singleton
+ fun bindSystemAudioRepository(repository: SystemAudioRepositoryImpl): SystemAudioRepository
+
+ @Binds
+ @Singleton
+ fun bindAppAudioLocalDataSource(dataSource: AppAudioLocalDataSource.Base): AppAudioLocalDataSource
+
+ @Binds
+ @Singleton
+ fun bindAppAudioRepository(repository: AppAudioRepositoryImpl): AppAudioRepository
+
+ @Binds
+ @Singleton
+ fun bindVideosRepository(repository: VideosRepositoryImpl): VideosRepository
+
+ @Binds
+ @Singleton
+ fun bindVideosLocalDataSource(dataSource: VideosLocalDataSource.Base): VideosLocalDataSource
+
+ @Binds
+ @Singleton
+ fun bindAudioStoreManager(manager: AudioStoreManager.Base): AudioStoreManager
+
+ @Binds
+ @Singleton
+ fun bindVideosStoreManager(manager: VideosStoreManager.Base): VideosStoreManager
+
+ @Binds
+ @Singleton
+ fun bindMediaQueryParser(manager: MediaQueryParser.Base): MediaQueryParser
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/di/modules/DependenciesModule.kt b/app/src/main/java/ru/aleshin/mixplayer/di/modules/DependenciesModule.kt
new file mode 100644
index 0000000..fe2710e
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/di/modules/DependenciesModule.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.mixplayer.di.modules
+
+import dagger.Binds
+import dagger.Module
+import ru.aleshin.features.home.impl.di.HomeFeatureDependencies
+import ru.aleshin.features.player.impl.di.PlayerFeatureDependencies
+import ru.aleshin.features.settings.impl.di.SettingsFeatureDependencies
+import ru.aleshin.mixplayer.di.component.AppComponent
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Module
+interface DependenciesModule {
+
+ @Binds
+ fun bindHomeFeatureDependencies(deps: AppComponent): HomeFeatureDependencies
+
+ @Binds
+ fun bindPlayerFeatureDependencies(deps: AppComponent): PlayerFeatureDependencies
+
+ @Binds
+ fun bindSettingsFeatureDependencies(deps: AppComponent): SettingsFeatureDependencies
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/di/modules/DomainModules.kt b/app/src/main/java/ru/aleshin/mixplayer/di/modules/DomainModules.kt
new file mode 100644
index 0000000..14c7f35
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/di/modules/DomainModules.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.mixplayer.di.modules
+
+import dagger.Binds
+import dagger.Module
+import ru.aleshin.mixplayer.domain.interactors.SettingsInteractor
+import ru.aleshin.mixplayer.domain.common.MainEitherWrapper
+import ru.aleshin.mixplayer.domain.common.MainErrorHandler
+import javax.inject.Singleton
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Module
+interface DomainModules {
+
+ @Binds
+ @Singleton
+ fun bindSettingsInteractor(interactor: SettingsInteractor.Base): SettingsInteractor
+
+ @Binds
+ @Singleton
+ fun bindMainEitherWrapper(wrapper: MainEitherWrapper.Base): MainEitherWrapper
+
+ @Binds
+ @Singleton
+ fun bindMainErrorHandler(handler: MainErrorHandler.Base): MainErrorHandler
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/di/modules/FeatureModule.kt b/app/src/main/java/ru/aleshin/mixplayer/di/modules/FeatureModule.kt
new file mode 100644
index 0000000..20b3dff
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/di/modules/FeatureModule.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.mixplayer.di.modules
+
+import dagger.Module
+import dagger.Provides
+import ru.aleshin.features.home.impl.di.HomeFeatureDependencies
+import ru.aleshin.features.home.impl.di.holder.HomeComponentHolder
+import ru.aleshin.features.player.impl.di.PlayerFeatureDependencies
+import ru.aleshin.features.player.impl.di.holder.PlayerComponentHolder
+import ru.aleshin.features.settings.impl.di.SettingsFeatureDependencies
+import ru.aleshin.features.settings.impl.di.holder.SettingsComponentHolder
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Module
+class FeatureModule {
+
+ @Provides
+ fun provideHomeStarter(
+ dependencies: HomeFeatureDependencies,
+ ) = HomeComponentHolder.let {
+ it.init(dependencies)
+ it.fetchApi().fetchStarter()
+ }
+
+ @Provides
+ fun providePlayerStarter(
+ dependencies: PlayerFeatureDependencies,
+ ) = PlayerComponentHolder.let {
+ it.init(dependencies)
+ it.fetchApi().fetchStarter()
+ }
+
+ @Provides
+ fun provideSettingsStarter(
+ dependencies: SettingsFeatureDependencies,
+ ) = SettingsComponentHolder.let {
+ it.init(dependencies)
+ it.fetchApi().fetchStarter()
+ }
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/di/modules/NavigationModule.kt b/app/src/main/java/ru/aleshin/mixplayer/di/modules/NavigationModule.kt
new file mode 100644
index 0000000..4b58d46
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/di/modules/NavigationModule.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.mixplayer.di.modules
+
+import dagger.Module
+import dagger.Provides
+import ru.aleshin.core.common.navigation.CommandBuffer
+import ru.aleshin.core.common.navigation.Router
+import ru.aleshin.core.common.navigation.navigator.NavigatorManager
+import javax.inject.Singleton
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Module
+class NavigationModule {
+
+ // Global Navigator
+
+ @Provides
+ @Singleton
+ fun provideGlobalNavigatorManager(commandBuffer: CommandBuffer): NavigatorManager =
+ NavigatorManager.Base(commandBuffer)
+
+ @Provides
+ @Singleton
+ fun provideGlobalCommandBuffer(): CommandBuffer =
+ CommandBuffer.Base()
+
+ @Provides
+ @Singleton
+ fun provideGlobalRouter(commandBuffer: CommandBuffer): Router =
+ Router.Base(commandBuffer)
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/di/modules/PresentationModule.kt b/app/src/main/java/ru/aleshin/mixplayer/di/modules/PresentationModule.kt
new file mode 100644
index 0000000..94a6293
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/di/modules/PresentationModule.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.di.modules
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import dagger.Binds
+import dagger.Module
+import ru.aleshin.features.player.api.presentation.common.MediaCommunicator
+import ru.aleshin.features.player.api.presentation.common.MediaController
+import ru.aleshin.features.player.api.presentation.common.PlaybackCommunicator
+import ru.aleshin.features.player.api.presentation.common.PlaybackManager
+import ru.aleshin.mixplayer.navigation.GlobalNavigationManager
+import ru.aleshin.mixplayer.presentation.ui.main.viewmodel.MainEffectCommunicator
+import ru.aleshin.mixplayer.presentation.ui.main.viewmodel.MainStateCommunicator
+import ru.aleshin.mixplayer.presentation.ui.main.viewmodel.MainViewModel
+import ru.aleshin.mixplayer.presentation.ui.main.viewmodel.MainWorkProcessor
+import ru.aleshin.mixplayer.presentation.ui.main.viewmodel.NavigationWorkProcessor
+import javax.inject.Singleton
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Module
+interface PresentationModule {
+
+ // Common
+
+ @Binds
+ @Singleton
+ fun bindNavigationManager(manager: GlobalNavigationManager.Base): GlobalNavigationManager
+
+ @Binds
+ @Singleton
+ fun bindMediaCommunicator(communicator: MediaCommunicator.Base): MediaCommunicator
+
+ @Binds
+ @Singleton
+ fun bindPlaybackCommunicator(communicator: PlaybackCommunicator.Base): PlaybackCommunicator
+
+ @Binds
+ @Singleton
+ fun bindPlaybackManager(manager: PlaybackManager.Base): PlaybackManager
+
+ @Binds
+ @Singleton
+ fun bindMediaController(controller: MediaController.Base): MediaController
+
+ // Main Activity
+
+ @Binds
+ fun bindMainViewModelFactory(factory: MainViewModel.Factory): ViewModelProvider.Factory
+
+ @Binds
+ fun bindMainViewModel(viewModel: MainViewModel): ViewModel
+
+ @Binds
+ @Singleton
+ fun bindMainStateCommunicator(communicator: MainStateCommunicator.Base): MainStateCommunicator
+
+ @Binds
+ @Singleton
+ fun bindMainEffectCommunicator(communicator: MainEffectCommunicator.Base): MainEffectCommunicator
+
+ @Binds
+ fun bindMainWorkProcessor(processor: MainWorkProcessor.Base): MainWorkProcessor
+
+ @Binds
+ fun bindNavigationWorkProcessor(processor: NavigationWorkProcessor.Base): NavigationWorkProcessor
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/domain/common/MainEitherWrapper.kt b/app/src/main/java/ru/aleshin/mixplayer/domain/common/MainEitherWrapper.kt
new file mode 100644
index 0000000..74c8137
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/domain/common/MainEitherWrapper.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.domain.common
+
+import ru.aleshin.core.common.wrappers.FlowEitherWrapper
+import ru.aleshin.mixplayer.domain.entity.MainFailures
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 13.06.2023.
+ */
+interface MainEitherWrapper : FlowEitherWrapper {
+
+ class Base @Inject constructor(
+ errorHandler: MainErrorHandler,
+ ) : MainEitherWrapper, FlowEitherWrapper.Abstract(errorHandler)
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/domain/common/MainErrorHandler.kt b/app/src/main/java/ru/aleshin/mixplayer/domain/common/MainErrorHandler.kt
new file mode 100644
index 0000000..4092a25
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/domain/common/MainErrorHandler.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.domain.common
+
+import ru.aleshin.core.common.handlers.ErrorHandler
+import ru.aleshin.mixplayer.domain.entity.MainFailures
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 13.06.2023.
+ */
+interface MainErrorHandler : ErrorHandler {
+ class Base @Inject constructor() : MainErrorHandler {
+ override fun handle(throwable: Throwable) = when (throwable) {
+ else -> MainFailures.OtherError(throwable)
+ }
+ }
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/domain/entity/MainFailures.kt b/app/src/main/java/ru/aleshin/mixplayer/domain/entity/MainFailures.kt
new file mode 100644
index 0000000..bb0f78c
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/domain/entity/MainFailures.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.domain.entity
+
+import ru.aleshin.core.common.functional.DomainFailures
+
+/**
+ * @author Stanislav Aleshin on 13.06.2023.
+ */
+sealed class MainFailures : DomainFailures {
+ data class OtherError(val throwable: Throwable) : MainFailures()
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/domain/interactors/SettingsInteractor.kt b/app/src/main/java/ru/aleshin/mixplayer/domain/interactors/SettingsInteractor.kt
new file mode 100644
index 0000000..d3cc045
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/domain/interactors/SettingsInteractor.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.domain.interactors
+
+import kotlinx.coroutines.flow.Flow
+import ru.aleshin.core.common.functional.Either
+import ru.aleshin.features.settings.api.domain.entities.MixPlayerSettings
+import ru.aleshin.features.settings.api.domain.repositories.SettingsRepository
+import ru.aleshin.mixplayer.domain.common.MainEitherWrapper
+import ru.aleshin.mixplayer.domain.entity.MainFailures
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 14.06.2023.
+ */
+interface SettingsInteractor {
+
+ suspend fun fetchThemeSettings(): Flow>
+
+ class Base @Inject constructor(
+ private val settingsRepository: SettingsRepository,
+ private val eitherWrapper: MainEitherWrapper,
+ ) : SettingsInteractor {
+
+ override suspend fun fetchThemeSettings() = eitherWrapper.wrapFlow {
+ settingsRepository.fetchSettingsFlow()
+ }
+ }
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/navigation/GlobalNavigationManager.kt b/app/src/main/java/ru/aleshin/mixplayer/navigation/GlobalNavigationManager.kt
new file mode 100644
index 0000000..ead381f
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/navigation/GlobalNavigationManager.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.navigation
+
+import ru.aleshin.core.common.navigation.Router
+import ru.aleshin.features.home.api.di.HomeScreens
+import ru.aleshin.features.home.api.navigation.HomeFeatureStarter
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * @author Stanislav Aleshin on 13.06.2023.
+ */
+interface GlobalNavigationManager {
+
+ suspend fun navigateToHome(homeScreen: HomeScreens = HomeScreens.Home)
+
+ class Base @Inject constructor(
+ private val homeStarter: Provider,
+ private val globalRouter: Router,
+ ) : GlobalNavigationManager {
+
+ override suspend fun navigateToHome(homeScreen: HomeScreens) {
+ val screen = homeStarter.get().fetchHomeScreen(homeScreen)
+ globalRouter.replaceTo(screen = screen, isAll = true)
+ }
+ }
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/MainActivity.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/MainActivity.kt
new file mode 100644
index 0000000..09f902e
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/MainActivity.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.presentation.ui.main
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.lifecycleScope
+import cafe.adriel.voyager.navigator.CurrentScreen
+import kotlinx.coroutines.launch
+import ru.aleshin.core.common.navigation.navigator.AppNavigator
+import ru.aleshin.core.common.navigation.navigator.NavigatorManager
+import ru.aleshin.core.common.platform.activity.BaseActivity
+import ru.aleshin.core.common.platform.screen.ScreenContent
+import ru.aleshin.core.ui.theme.MixPlayerRes
+import ru.aleshin.core.ui.theme.MixPlayerTheme
+import ru.aleshin.features.player.api.presentation.player.PlayerService
+import ru.aleshin.features.player.api.presentation.player.PlayerServiceBinder
+import ru.aleshin.mixplayer.R
+import ru.aleshin.mixplayer.application.fetchApp
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainAction
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainEffect
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainEvent
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainViewState
+import ru.aleshin.mixplayer.presentation.ui.main.viewmodel.MainViewModel
+import ru.aleshin.mixplayer.presentation.ui.mapper.mapToMessage
+import ru.aleshin.mixplayer.presentation.ui.splash.SplashScreen
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+class MainActivity : BaseActivity() {
+
+ @Inject
+ lateinit var navigatorManager: NavigatorManager
+
+ @Inject
+ lateinit var viewModelFactory: MainViewModel.Factory
+
+ private var playerService: PlayerService? = null
+
+ private val serviceConnection = object : ServiceConnection {
+
+ override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
+ val playerBinder = binder as PlayerServiceBinder
+ playerService = playerBinder.fetchService()
+ lifecycleScope.launch {
+ playerService?.collectInfo { viewModel.dispatchEvent(MainEvent.SendPlayerInfo(it)) }
+ }
+ lifecycleScope.launch {
+ playerService?.collectErrors { viewModel.dispatchEvent(MainEvent.OnPlayerError(it)) }
+ }
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ playerService = null
+ }
+ }
+
+ override fun initDI() = fetchApp().appComponent.inject(this)
+
+ @Composable
+ override fun Content() = ScreenContent(viewModel, MainViewState()) { state ->
+ MixPlayerTheme(
+ themeType = state.generalSettings.themeUiType,
+ language = state.generalSettings.languageUiType,
+ ) {
+ val hostState = remember { SnackbarHostState() }
+ val coreStrings = MixPlayerRes.strings
+
+ AppNavigator(
+ initialScreen = SplashScreen,
+ navigatorManager = navigatorManager,
+ ) {
+ Scaffold(
+ content = { Box(Modifier.padding(it), content = { CurrentScreen() }) },
+ snackbarHost = { SnackbarHost(hostState) }
+ )
+ }
+
+ handleEffect { effect ->
+ when (effect) {
+ is MainEffect.WorkMediaCommand -> {
+ playerService?.workMediaCommand(effect.command)
+ }
+ is MainEffect.ShowPlayerError -> {
+ hostState.showSnackbar(
+ message = effect.error.mapToMessage(coreStrings),
+ withDismissAction = true,
+ )
+ }
+ }
+ }
+ }
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ setTheme(R.style.Theme_MixPlayer)
+ bindService(fetchApp().serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE)
+ }
+
+ override fun onStop() {
+ unbindService(serviceConnection)
+ super.onStop()
+ }
+
+ override fun onDestroy() {
+ if (isFinishing) stopService(fetchApp().serviceIntent)
+ super.onDestroy()
+ }
+
+ override fun fetchViewModelFactory() = viewModelFactory
+
+ override fun fetchViewModelClass() = MainViewModel::class.java
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/contract/MainContract.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/contract/MainContract.kt
new file mode 100644
index 0000000..ed356a9
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/contract/MainContract.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.mixplayer.presentation.ui.main.contract
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import ru.aleshin.core.common.functional.MediaCommand
+import ru.aleshin.core.common.functional.audio.PlayerError
+import ru.aleshin.core.common.functional.audio.PlayerInfo
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+import ru.aleshin.mixplayer.presentation.ui.models.GeneralSettingsUi
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Parcelize
+data class MainViewState(
+ val generalSettings: GeneralSettingsUi = GeneralSettingsUi(),
+) : BaseViewState, Parcelable
+
+sealed class MainEvent : BaseEvent {
+ object Init : MainEvent()
+ data class SendPlayerInfo(val info: PlayerInfo) : MainEvent()
+ data class OnPlayerError(val error: PlayerError) : MainEvent()
+}
+
+sealed class MainEffect : BaseUiEffect {
+ data class WorkMediaCommand(val command: MediaCommand) : MainEffect()
+ data class ShowPlayerError(val error: PlayerError) : MainEffect()
+}
+
+sealed class MainAction : BaseAction {
+ data class ChangeGeneralSettings(val generalSettings: GeneralSettingsUi) : MainAction()
+ object Navigate : MainAction()
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainEffectCommunicator.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainEffectCommunicator.kt
new file mode 100644
index 0000000..6209d3f
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainEffectCommunicator.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.presentation.ui.main.viewmodel
+
+import ru.aleshin.core.common.platform.communications.state.EffectCommunicator
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainEffect
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 13.06.2023.
+ */
+interface MainEffectCommunicator : EffectCommunicator {
+
+ class Base @Inject constructor() : MainEffectCommunicator,
+ EffectCommunicator.Abstract()
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainStateCommunicator.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainStateCommunicator.kt
new file mode 100644
index 0000000..a871d87
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainStateCommunicator.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.mixplayer.presentation.ui.main.viewmodel
+
+import ru.aleshin.core.common.platform.communications.state.StateCommunicator
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainViewState
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 13.06.2023.
+ */
+interface MainStateCommunicator : StateCommunicator {
+
+ class Base @Inject constructor() : MainStateCommunicator,
+ StateCommunicator.Abstract(defaultState = MainViewState())
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainViewModel.kt
new file mode 100644
index 0000000..ebb704c
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainViewModel.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.mixplayer.presentation.ui.main.viewmodel
+
+import ru.aleshin.core.common.managers.CoroutineManager
+import ru.aleshin.core.common.platform.communications.state.EffectCommunicator
+import ru.aleshin.core.common.platform.screenmodel.BaseViewModel
+import ru.aleshin.core.common.platform.screenmodel.work.WorkScope
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainAction
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainEffect
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainEvent
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainViewState
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+class MainViewModel @Inject constructor(
+ private val mainWorkProcessor: MainWorkProcessor,
+ private val navigationWorkProcessor: NavigationWorkProcessor,
+ stateCommunicator: MainStateCommunicator,
+ effectCommunicator: MainEffectCommunicator,
+ coroutineManager: CoroutineManager,
+) : BaseViewModel(
+ stateCommunicator = stateCommunicator,
+ effectCommunicator = effectCommunicator,
+ coroutineManager = coroutineManager,
+) {
+
+ override fun init() {
+ if (!isInitialize.get()) {
+ super.init()
+ dispatchEvent(MainEvent.Init)
+ }
+ }
+
+ override suspend fun WorkScope.handleEvent(event: MainEvent) {
+ when (event) {
+ is MainEvent.Init -> {
+ launchBackgroundWork(MainWorkCommand.LoadGeneralMain) {
+ mainWorkProcessor.work(MainWorkCommand.LoadGeneralMain).collectAndHandleWork()
+ }
+ launchBackgroundWork(MainWorkCommand.ReceiveMediaCommand) {
+ mainWorkProcessor.work(MainWorkCommand.ReceiveMediaCommand).collectAndHandleWork()
+ }
+ navigationWorkProcessor.work(NavigationWorkCommand.NavigateToHome).handleWork()
+ }
+ is MainEvent.SendPlayerInfo -> {
+ mainWorkProcessor.work(MainWorkCommand.SendPlayerInfo(event.info)).collectAndHandleWork()
+ }
+ is MainEvent.OnPlayerError -> sendEffect(MainEffect.ShowPlayerError(event.error))
+ }
+ }
+
+ override suspend fun reduce(
+ action: MainAction,
+ currentState: MainViewState,
+ ) = when (action) {
+ is MainAction.Navigate -> currentState
+ is MainAction.ChangeGeneralSettings -> currentState.copy(
+ generalSettings = action.generalSettings,
+ )
+ }
+
+ class Factory @Inject constructor(viewModel: Provider) :
+ BaseViewModel.Factory(viewModel)
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainWorkProcessor.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainWorkProcessor.kt
new file mode 100644
index 0000000..1fe0b40
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/MainWorkProcessor.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.mixplayer.presentation.ui.main.viewmodel
+
+import android.util.Log
+import kotlinx.coroutines.flow.flow
+import ru.aleshin.core.common.functional.audio.PlayerInfo
+import ru.aleshin.core.common.functional.handle
+import ru.aleshin.core.common.platform.screenmodel.work.ActionResult
+import ru.aleshin.core.common.platform.screenmodel.work.EffectResult
+import ru.aleshin.core.common.platform.screenmodel.work.FlowWorkProcessor
+import ru.aleshin.core.common.platform.screenmodel.work.WorkCommand
+import ru.aleshin.core.common.platform.screenmodel.work.WorkResult
+import ru.aleshin.features.player.api.presentation.common.MediaController
+import ru.aleshin.features.player.api.presentation.common.PlaybackManager
+import ru.aleshin.mixplayer.presentation.ui.mapper.mapToUi
+import ru.aleshin.mixplayer.domain.interactors.SettingsInteractor
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainAction
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainEffect
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface MainWorkProcessor : FlowWorkProcessor {
+
+ class Base @Inject constructor(
+ private val settingsInteractor: SettingsInteractor,
+ private val mediaController: MediaController,
+ private val playbackManager: PlaybackManager,
+ ) : MainWorkProcessor {
+
+ override suspend fun work(command: MainWorkCommand) = when (command) {
+ is MainWorkCommand.LoadGeneralMain -> loadGeneralSettingsWork()
+ is MainWorkCommand.ReceiveMediaCommand -> receiveMediaCommandWork()
+ is MainWorkCommand.SendPlayerInfo -> sendPlayerInfoWork(command.info)
+ }
+
+ private fun loadGeneralSettingsWork() = flow {
+ settingsInteractor.fetchThemeSettings().collect { settingsEither ->
+ settingsEither.handle(
+ onLeftAction = { error(RuntimeException("Error get GeneralSettings -> $it")) },
+ onRightAction = {
+ val action = MainAction.ChangeGeneralSettings(it.general.mapToUi())
+ emit(ActionResult(action))
+ },
+ )
+ }
+ }
+
+ private fun receiveMediaCommandWork() = flow {
+ mediaController.collectCommands { command ->
+ emit(EffectResult(MainEffect.WorkMediaCommand(command)))
+ }
+ }
+
+ private fun sendPlayerInfoWork(info: PlayerInfo) = flow> {
+ playbackManager.sendInfo(info)
+ }
+ }
+}
+
+sealed class MainWorkCommand : WorkCommand {
+ object LoadGeneralMain : MainWorkCommand()
+ object ReceiveMediaCommand : MainWorkCommand()
+ data class SendPlayerInfo(val info: PlayerInfo) : MainWorkCommand()
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/NavigationWorkProcessor.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/NavigationWorkProcessor.kt
new file mode 100644
index 0000000..b6a587a
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/main/viewmodel/NavigationWorkProcessor.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.presentation.ui.main.viewmodel
+
+import kotlinx.coroutines.delay
+import ru.aleshin.core.common.functional.Constants
+import ru.aleshin.core.common.platform.screenmodel.work.ActionResult
+import ru.aleshin.core.common.platform.screenmodel.work.WorkCommand
+import ru.aleshin.core.common.platform.screenmodel.work.WorkProcessor
+import ru.aleshin.mixplayer.navigation.GlobalNavigationManager
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainAction
+import ru.aleshin.mixplayer.presentation.ui.main.contract.MainEffect
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 04.07.2023.
+ */
+interface NavigationWorkProcessor : WorkProcessor {
+
+ class Base @Inject constructor(
+ private val navigationManager: GlobalNavigationManager,
+ ) : NavigationWorkProcessor {
+
+ override suspend fun work(command: NavigationWorkCommand) = when (command) {
+ NavigationWorkCommand.NavigateToHome -> {
+ delay(Constants.Delay.SPLASH)
+ ActionResult(MainAction.Navigate).apply { navigationManager.navigateToHome() }
+ }
+ }
+ }
+}
+
+sealed class NavigationWorkCommand : WorkCommand {
+ object NavigateToHome : NavigationWorkCommand()
+}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/mapper/PlayerErrorsMapper.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/mapper/PlayerErrorsMapper.kt
new file mode 100644
index 0000000..f11c908
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/mapper/PlayerErrorsMapper.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.presentation.ui.mapper
+
+import ru.aleshin.core.common.functional.audio.PlayerError
+import ru.aleshin.core.ui.theme.tokens.MixPlayerStrings
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+fun PlayerError.mapToMessage(string: MixPlayerStrings) = when (this) {
+ PlayerError.DATA_SOURCE -> string.dataSourcePlayerError
+ PlayerError.AUDIO_FOCUS -> string.audioFocusPlayerError
+ PlayerError.OTHER -> string.otherPlayerError
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/mapper/SettingsDomainToUiMapper.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/mapper/SettingsDomainToUiMapper.kt
new file mode 100644
index 0000000..c95f103
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/mapper/SettingsDomainToUiMapper.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.presentation.ui.mapper
+
+import ru.aleshin.core.ui.theme.material.ThemeUiType
+import ru.aleshin.core.ui.theme.tokens.LanguageUiType
+import ru.aleshin.features.settings.api.domain.entities.GeneralSettings
+import ru.aleshin.features.settings.api.domain.entities.LanguageType
+import ru.aleshin.features.settings.api.domain.entities.ThemeType
+import ru.aleshin.mixplayer.presentation.ui.models.GeneralSettingsUi
+
+/**
+ * @author Stanislav Aleshin on 14.06.2023.
+ */
+fun GeneralSettings.mapToUi() = GeneralSettingsUi(
+ languageUiType = when (languageType) {
+ LanguageType.DEFAULT -> LanguageUiType.DEFAULT
+ LanguageType.EN -> LanguageUiType.EN
+ LanguageType.RU -> LanguageUiType.RU
+ },
+ themeUiType = when (themeType) {
+ ThemeType.DEFAULT -> ThemeUiType.DEFAULT
+ ThemeType.LIGHT -> ThemeUiType.LIGHT
+ ThemeType.DARK -> ThemeUiType.DARK
+ },
+)
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/models/GeneralSettingsUi.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/models/GeneralSettingsUi.kt
new file mode 100644
index 0000000..fbe72ad
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/models/GeneralSettingsUi.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.presentation.ui.models
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import ru.aleshin.core.ui.theme.material.ThemeUiType
+import ru.aleshin.core.ui.theme.tokens.LanguageUiType
+
+/**
+ * @author Stanislav Aleshin on 14.06.2023.
+ */
+@Parcelize
+data class GeneralSettingsUi(
+ val languageUiType: LanguageUiType = LanguageUiType.DEFAULT,
+ val themeUiType: ThemeUiType = ThemeUiType.DEFAULT,
+) : Parcelable
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/splash/SplashContent.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/splash/SplashContent.kt
new file mode 100644
index 0000000..eb2c9d6
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/splash/SplashContent.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.presentation.ui.splash
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.delay
+import ru.aleshin.core.common.functional.Constants
+import ru.aleshin.core.ui.theme.MixPlayerRes
+import ru.aleshin.core.ui.theme.MixPlayerTheme
+import ru.aleshin.core.ui.theme.material.ThemeUiType
+import ru.aleshin.core.ui.theme.material.onSplash
+import ru.aleshin.core.ui.theme.material.splash
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+@Composable
+fun SplashContent(modifier: Modifier = Modifier) {
+ var isVisible by rememberSaveable { mutableStateOf(false) }
+ Box(
+ modifier = modifier.fillMaxSize().background(splash),
+ contentAlignment = Alignment.Center,
+ ) {
+ Column(
+ modifier = Modifier,
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Icon(
+ modifier = Modifier.size(200.dp),
+ painter = painterResource(id = MixPlayerRes.icons.launcher),
+ contentDescription = MixPlayerRes.strings.appName,
+ tint = onSplash,
+ )
+ AnimatedVisibility(
+ enter = fadeIn() + expandVertically(),
+ visible = isVisible,
+ ) {
+ Text(
+ text = MixPlayerRes.strings.appNameSplash,
+ color = onSplash,
+ style = MaterialTheme.typography.displaySmall.copy(
+ fontWeight = FontWeight.ExtraBold,
+ ),
+ )
+ }
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ delay(Constants.Delay.SPLASH_TEXT)
+ isVisible = true
+ }
+}
+
+//@Composable
+//@Preview
+//private fun SplashContent_Preview() {
+// MixPlayerTheme(themeType = ThemeUiType.LIGHT) {
+// SplashContent(
+// modifier = Modifier,
+// )
+// }
+//}
diff --git a/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/splash/SplashScreen.kt b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/splash/SplashScreen.kt
new file mode 100644
index 0000000..b1466c7
--- /dev/null
+++ b/app/src/main/java/ru/aleshin/mixplayer/presentation/ui/splash/SplashScreen.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.mixplayer.presentation.ui.splash
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import cafe.adriel.voyager.core.screen.Screen
+import com.google.accompanist.systemuicontroller.rememberSystemUiController
+import ru.aleshin.core.ui.theme.material.splash
+
+/**
+ * @author Stanislav Aleshin on 11.07.2023.
+ */
+object SplashScreen : Screen {
+
+ @Composable
+ override fun Content() {
+ SplashSystemUi()
+ SplashContent()
+ }
+
+ @Composable
+ private fun SplashSystemUi() {
+ val systemUiController = rememberSystemUiController()
+
+ SideEffect {
+ systemUiController.setNavigationBarColor(color = splash, darkIcons = false)
+ systemUiController.setStatusBarColor(color = splash, darkIcons = false)
+ }
+ }
+}
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..fd8887c
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..97b0b22
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..8ce17f0
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..1a7e06d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..caaccf3
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..4d255ea
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..ef19848
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..c301a01
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..ae49bde
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..2af6fb7
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..d0778e2
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..696b3e0
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..b5349e5
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..f87f555
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..121bd87
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..f040582
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #3161DB
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..4b6adee
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ MixPlayer
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..bbef023
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/ru/aleshin/mixplayer/ExampleUnitTest.kt b/app/src/test/java/ru/aleshin/mixplayer/ExampleUnitTest.kt
new file mode 100644
index 0000000..115adb1
--- /dev/null
+++ b/app/src/test/java/ru/aleshin/mixplayer/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package ru.aleshin.mixplayer
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..8c4eed0
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ mavenCentral()
+ mavenLocal()
+ gradlePluginPortal()
+ google()
+}
+
+dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
+ implementation("com.android.tools.build:gradle:8.2.0-alpha10")
+}
diff --git a/buildSrc/build/classes/kotlin/main/Config.class b/buildSrc/build/classes/kotlin/main/Config.class
new file mode 100644
index 0000000..823ecab
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Config.class differ
diff --git a/buildSrc/build/classes/kotlin/main/Dependencies$AndroidX.class b/buildSrc/build/classes/kotlin/main/Dependencies$AndroidX.class
new file mode 100644
index 0000000..203d9e2
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Dependencies$AndroidX.class differ
diff --git a/buildSrc/build/classes/kotlin/main/Dependencies$Compose.class b/buildSrc/build/classes/kotlin/main/Dependencies$Compose.class
new file mode 100644
index 0000000..f4fb880
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Dependencies$Compose.class differ
diff --git a/buildSrc/build/classes/kotlin/main/Dependencies$Dagger.class b/buildSrc/build/classes/kotlin/main/Dependencies$Dagger.class
new file mode 100644
index 0000000..75dc9ff
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Dependencies$Dagger.class differ
diff --git a/buildSrc/build/classes/kotlin/main/Dependencies$ExoPlayer.class b/buildSrc/build/classes/kotlin/main/Dependencies$ExoPlayer.class
new file mode 100644
index 0000000..e9ea6dd
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Dependencies$ExoPlayer.class differ
diff --git a/buildSrc/build/classes/kotlin/main/Dependencies$Glide.class b/buildSrc/build/classes/kotlin/main/Dependencies$Glide.class
new file mode 100644
index 0000000..4d7f76e
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Dependencies$Glide.class differ
diff --git a/buildSrc/build/classes/kotlin/main/Dependencies$Leakcanary.class b/buildSrc/build/classes/kotlin/main/Dependencies$Leakcanary.class
new file mode 100644
index 0000000..544e035
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Dependencies$Leakcanary.class differ
diff --git a/buildSrc/build/classes/kotlin/main/Dependencies$Room.class b/buildSrc/build/classes/kotlin/main/Dependencies$Room.class
new file mode 100644
index 0000000..9738ad7
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Dependencies$Room.class differ
diff --git a/buildSrc/build/classes/kotlin/main/Dependencies$Test.class b/buildSrc/build/classes/kotlin/main/Dependencies$Test.class
new file mode 100644
index 0000000..36ee71b
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Dependencies$Test.class differ
diff --git a/buildSrc/build/classes/kotlin/main/Dependencies$Voyager.class b/buildSrc/build/classes/kotlin/main/Dependencies$Voyager.class
new file mode 100644
index 0000000..dadf16c
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Dependencies$Voyager.class differ
diff --git a/buildSrc/build/classes/kotlin/main/Dependencies.class b/buildSrc/build/classes/kotlin/main/Dependencies.class
new file mode 100644
index 0000000..37526f4
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Dependencies.class differ
diff --git a/buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module b/buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module
new file mode 100644
index 0000000..3aa6361
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module differ
diff --git a/buildSrc/build/classes/kotlin/main/Versions.class b/buildSrc/build/classes/kotlin/main/Versions.class
new file mode 100644
index 0000000..58d77cb
Binary files /dev/null and b/buildSrc/build/classes/kotlin/main/Versions.class differ
diff --git a/buildSrc/build/kotlin/buildSrcjar-classes.txt b/buildSrc/build/kotlin/buildSrcjar-classes.txt
new file mode 100644
index 0000000..6e2b81a
--- /dev/null
+++ b/buildSrc/build/kotlin/buildSrcjar-classes.txt
@@ -0,0 +1 @@
+/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Config.class:/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Dependencies$AndroidX.class:/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Dependencies$Compose.class:/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Dependencies$Dagger.class:/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Dependencies$ExoPlayer.class:/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Dependencies$Glide.class:/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Dependencies$Leakcanary.class:/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Dependencies$Room.class:/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Dependencies$Test.class:/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Dependencies$Voyager.class:/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Dependencies.class:/Users/v1tzor/AndroidStudioProjects/MixPlayer/buildSrc/build/classes/kotlin/main/Versions.class
\ No newline at end of file
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab
new file mode 100644
index 0000000..f28030f
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream
new file mode 100644
index 0000000..b121b79
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len
new file mode 100644
index 0000000..05301cb
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len
new file mode 100644
index 0000000..a9f80ae
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at
new file mode 100644
index 0000000..c2b49d1
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i
new file mode 100644
index 0000000..8e1c13a
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len
new file mode 100644
index 0000000..131e265
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab
new file mode 100644
index 0000000..74e23f0
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream
new file mode 100644
index 0000000..955de09
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len
new file mode 100644
index 0000000..5adef4e
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len
new file mode 100644
index 0000000..6f677df
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at
new file mode 100644
index 0000000..8e29fe1
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i
new file mode 100644
index 0000000..471c5f9
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len
new file mode 100644
index 0000000..131e265
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab
new file mode 100644
index 0000000..a75ba0d
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream
new file mode 100644
index 0000000..955de09
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len
new file mode 100644
index 0000000..5adef4e
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len
new file mode 100644
index 0000000..6f677df
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at
new file mode 100644
index 0000000..e19be5c
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i
new file mode 100644
index 0000000..471c5f9
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len
new file mode 100644
index 0000000..131e265
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab
new file mode 100644
index 0000000..4194749
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream
new file mode 100644
index 0000000..109b0d6
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len
new file mode 100644
index 0000000..9b86fcd
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.keystream.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len
new file mode 100644
index 0000000..a363176
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values
new file mode 100644
index 0000000..0cafe1a
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at
new file mode 100644
index 0000000..b0a20f8
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.at differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.s b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.s
new file mode 100644
index 0000000..6a4442c
--- /dev/null
+++ b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab.values.s
@@ -0,0 +1 @@
+.+
\ No newline at end of file
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i
new file mode 100644
index 0000000..98b3db4
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len
new file mode 100644
index 0000000..131e265
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/constants.tab_i.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab
new file mode 100644
index 0000000..5324214
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream
new file mode 100644
index 0000000..ae184b5
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len
new file mode 100644
index 0000000..5adef4e
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len
new file mode 100644
index 0000000..6f677df
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at
new file mode 100644
index 0000000..e19be5c
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i
new file mode 100644
index 0000000..9b08d5c
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len
new file mode 100644
index 0000000..131e265
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab
new file mode 100644
index 0000000..8835249
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream
new file mode 100644
index 0000000..50ae85a
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len
new file mode 100644
index 0000000..cf8a30a
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len
new file mode 100644
index 0000000..003bc0e
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at
new file mode 100644
index 0000000..eec89e0
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i
new file mode 100644
index 0000000..3077349
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len
new file mode 100644
index 0000000..131e265
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab
new file mode 100644
index 0000000..0dd6a25
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream
new file mode 100644
index 0000000..c1f7662
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len
new file mode 100644
index 0000000..05301cb
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len
new file mode 100644
index 0000000..a9f80ae
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at
new file mode 100644
index 0000000..3dc797f
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i
new file mode 100644
index 0000000..0b4427e
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len
new file mode 100644
index 0000000..131e265
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/counters.tab b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/counters.tab
new file mode 100644
index 0000000..8efbaaa
--- /dev/null
+++ b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/counters.tab
@@ -0,0 +1,2 @@
+19
+0
\ No newline at end of file
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab
new file mode 100644
index 0000000..6fd7293
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream
new file mode 100644
index 0000000..c1f7662
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len
new file mode 100644
index 0000000..05301cb
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len
new file mode 100644
index 0000000..a9f80ae
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at
new file mode 100644
index 0000000..dbc6bf9
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i
new file mode 100644
index 0000000..b969da8
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len
new file mode 100644
index 0000000..131e265
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab
new file mode 100644
index 0000000..078d344
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream
new file mode 100644
index 0000000..54555db
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len
new file mode 100644
index 0000000..2b895e7
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len
new file mode 100644
index 0000000..14f7c06
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at
new file mode 100644
index 0000000..053a6a2
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i
new file mode 100644
index 0000000..41018a9
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len
new file mode 100644
index 0000000..131e265
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab
new file mode 100644
index 0000000..4b675fe
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream
new file mode 100644
index 0000000..3fe5aff
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len
new file mode 100644
index 0000000..b67c227
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.len
new file mode 100644
index 0000000..b4da131
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at
new file mode 100644
index 0000000..45653e4
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i
new file mode 100644
index 0000000..837e31d
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len
new file mode 100644
index 0000000..131e265
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len differ
diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin b/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin
new file mode 100644
index 0000000..a9ba115
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin differ
diff --git a/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin b/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin
new file mode 100644
index 0000000..2a7e8ba
Binary files /dev/null and b/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin differ
diff --git a/buildSrc/build/libs/buildSrc.jar b/buildSrc/build/libs/buildSrc.jar
new file mode 100644
index 0000000..c54ef25
Binary files /dev/null and b/buildSrc/build/libs/buildSrc.jar differ
diff --git a/buildSrc/build/tmp/jar/MANIFEST.MF b/buildSrc/build/tmp/jar/MANIFEST.MF
new file mode 100644
index 0000000..59499bc
--- /dev/null
+++ b/buildSrc/build/tmp/jar/MANIFEST.MF
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+
diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt
new file mode 100644
index 0000000..43614df
--- /dev/null
+++ b/buildSrc/src/main/kotlin/Config.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+object Config {
+ const val applicationId = "ru.aleshin.mixplayer"
+
+ const val compileSdkVersion = 34
+ const val targetSdkVersion = 34
+ const val minSdkVersion = 24
+
+ const val versionCode = 1
+ const val versionName = "0.1-alpha"
+
+ const val testInstrumentRunner = "androidx.test.runner.AndroidJUnitRunner"
+ const val consumerProguardFiles = "consumer-rules.pro"
+
+ const val jvmTarget = "17"
+
+ const val kotlinCompiler = "1.4.6"
+}
diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt
new file mode 100644
index 0000000..15c4bfd
--- /dev/null
+++ b/buildSrc/src/main/kotlin/Dependencies.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+/**
+ * @author Stanislav Aleshin on 14.02.2023.
+ */
+object Dependencies {
+
+ object AndroidX {
+ const val core = "androidx.core:core-ktx:${Versions.core}"
+
+ const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
+ const val material = "androidx.compose.material3:material3:${Versions.material}"
+ const val googleMaterial = "com.google.android.material:material:${Versions.googleMaterial}"
+
+ const val systemUiController = "com.google.accompanist:accompanist-systemuicontroller:${Versions.accompanist}"
+ const val placeHolder = "com.google.accompanist:accompanist-placeholder-material:${Versions.accompanist}"
+ const val refresh = "com.google.accompanist:accompanist-swiperefresh:${Versions.accompanist}"
+ const val lifecycleViewModel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycle}"
+ const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
+ const val gson = "com.google.code.gson:gson:${Versions.gson}"
+ }
+
+ object ExoPlayer {
+ const val library = "androidx.media3:media3-exoplayer:${Versions.exoplayer}"
+ const val ui = "androidx.media3:media3-ui:${Versions.exoplayer}"
+ }
+
+ object Compose {
+ const val ui = "androidx.compose.ui:ui:${Versions.compose}"
+ const val foundation = "androidx.compose.foundation:foundation:${Versions.compose}"
+ const val layout = "androidx.compose.foundation:foundation-layout:${Versions.compose}"
+ const val activity = "androidx.activity:activity-compose:${Versions.activityCompose}"
+ const val preview = "androidx.compose.ui:ui-tooling-preview:${Versions.compose}"
+ const val uiTooling = "androidx.compose.ui:ui-tooling:${Versions.compose}"
+ const val uiTestManifest = "androidx.compose.ui:ui-test-manifest:${Versions.compose}"
+ }
+
+ object Voyager {
+ const val navigator = "cafe.adriel.voyager:voyager-navigator:${Versions.voyager}"
+ const val transitions = "cafe.adriel.voyager:voyager-transitions:${Versions.voyager}"
+ const val screenModel = "cafe.adriel.voyager:voyager-androidx:${Versions.voyager}"
+ }
+
+ object Leakcanary {
+ const val library = "com.squareup.leakcanary:leakcanary-android:${Versions.leakcanary}"
+ }
+
+ object Dagger {
+ const val core = "com.google.dagger:dagger:${Versions.dagger}"
+ const val kapt = "com.google.dagger:dagger-compiler:${Versions.dagger}"
+ }
+
+ object Room {
+ const val core = "androidx.room:room-runtime:${Versions.room}"
+ const val ktx = "androidx.room:room-ktx:${Versions.room}"
+ const val kapt = "androidx.room:room-compiler:${Versions.room}"
+ }
+
+ object Test {
+ const val jUnit = "junit:junit:${Versions.jUnit}"
+ const val composeJUnit = "androidx.compose.ui:ui-test-junit4:${Versions.compose}"
+ const val jUnitExt = "androidx.test.ext:junit:${Versions.jUnitExt}"
+ const val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutinesTest}"
+ const val turbine = "app.cash.turbine:turbine:${Versions.turbine}"
+ const val espresso = "androidx.test.espresso:espresso-core:${Versions.espresso}"
+ }
+
+ object Glide {
+ const val library = "com.github.bumptech.glide:compose:${Versions.glide}"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt
new file mode 100644
index 0000000..8c636b7
--- /dev/null
+++ b/buildSrc/src/main/kotlin/Versions.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+object Versions {
+ const val gson = "2.10.1"
+ const val core = "1.9.0"
+ const val appcompat = "1.6.1"
+ const val material = "1.2.0-alpha03"
+ const val googleMaterial = "1.8.0"
+ const val lifecycle = "2.5.1"
+ const val accompanist = "0.31.5-beta"
+ const val startup = "1.1.1"
+
+ const val exoplayer = "1.1.0"
+
+ const val charts = "0.3.5"
+
+ const val chartsHm = "1.0.1"
+ const val compose = "1.4.3"
+
+ const val activityCompose = "1.6.1"
+ const val voyager = "1.0.0-rc06"
+
+ const val dagger = "2.44.2"
+
+ const val room = "2.5.0"
+
+ const val glide = "1.0.0-alpha.1"
+
+ const val jUnit = "4.13.2"
+ const val jUnitExt = "1.1.4"
+ const val turbine = "0.12.1"
+ const val coroutinesTest = "1.6.1"
+ const val espresso = "3.5.0"
+
+ const val leakcanary = "2.10"
+}
diff --git a/core/common/.gitignore b/core/common/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/core/common/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts
new file mode 100644
index 0000000..6d9df04
--- /dev/null
+++ b/core/common/build.gradle.kts
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-parcelize")
+ kotlin("kapt")
+}
+
+repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ google()
+}
+
+android {
+ namespace = "ru.aleshin.core.common"
+ compileSdk = Config.compileSdkVersion
+
+ defaultConfig {
+ minSdk = Config.minSdkVersion
+
+ testInstrumentationRunner = Config.testInstrumentRunner
+ }
+
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro",
+ )
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = Config.jvmTarget
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Config.kotlinCompiler
+ }
+
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+ implementation(Dependencies.AndroidX.core)
+ implementation(Dependencies.AndroidX.lifecycleRuntime)
+ implementation(Dependencies.AndroidX.appcompat)
+ implementation(Dependencies.AndroidX.material)
+
+ implementation(Dependencies.Compose.ui)
+ implementation(Dependencies.Compose.activity)
+
+ implementation(Dependencies.Glide.library)
+
+ implementation(Dependencies.Dagger.core)
+ kapt(Dependencies.Dagger.kapt)
+
+ implementation(Dependencies.Voyager.navigator)
+ implementation(Dependencies.Voyager.screenModel)
+ implementation(Dependencies.Voyager.transitions)
+
+ testImplementation(Dependencies.Test.jUnit)
+ androidTestImplementation(Dependencies.Test.jUnitExt)
+ androidTestImplementation(Dependencies.Test.espresso)
+}
diff --git a/core/common/consumer-rules.pro b/core/common/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/core/common/proguard-rules.pro b/core/common/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/core/common/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/core/common/src/androidTest/java/ru/aleshin/core/common/ExampleInstrumentedTest.kt b/core/common/src/androidTest/java/ru/aleshin/core/common/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..483ed3a
--- /dev/null
+++ b/core/common/src/androidTest/java/ru/aleshin/core/common/ExampleInstrumentedTest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("ru.aleshin.core.common.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/core/common/src/main/AndroidManifest.xml b/core/common/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c193aa3
--- /dev/null
+++ b/core/common/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/common/src/main/java/ru/aleshin/core/common/di/ApplicationContext.kt b/core/common/src/main/java/ru/aleshin/core/common/di/ApplicationContext.kt
new file mode 100644
index 0000000..ac6e5bf
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/di/ApplicationContext.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.di
+
+import javax.inject.Qualifier
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Qualifier
+annotation class ApplicationContext
diff --git a/core/common/src/main/java/ru/aleshin/core/common/di/FeatureRouter.kt b/core/common/src/main/java/ru/aleshin/core/common/di/FeatureRouter.kt
new file mode 100644
index 0000000..aa28734
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/di/FeatureRouter.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.di
+
+import javax.inject.Qualifier
+
+/**
+ * @author Stanislav Aleshin on 04.07.2023.
+ */
+@Qualifier
+annotation class FeatureRouter
diff --git a/core/common/src/main/java/ru/aleshin/core/common/di/FeatureScope.kt b/core/common/src/main/java/ru/aleshin/core/common/di/FeatureScope.kt
new file mode 100644
index 0000000..dfe508b
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/di/FeatureScope.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.di
+
+import javax.inject.Scope
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Scope
+annotation class FeatureScope
diff --git a/core/common/src/main/java/ru/aleshin/core/common/di/ScreenKey.kt b/core/common/src/main/java/ru/aleshin/core/common/di/ScreenKey.kt
new file mode 100644
index 0000000..cb9c0a0
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/di/ScreenKey.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.di
+
+import cafe.adriel.voyager.core.screen.Screen
+import javax.inject.Qualifier
+import kotlin.reflect.KClass
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Qualifier
+annotation class ScreenKey(val screen: KClass)
diff --git a/core/common/src/main/java/ru/aleshin/core/common/di/ScreenModelKey.kt b/core/common/src/main/java/ru/aleshin/core/common/di/ScreenModelKey.kt
new file mode 100644
index 0000000..faa53cb
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/di/ScreenModelKey.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.di
+
+import cafe.adriel.voyager.core.model.ScreenModel
+import javax.inject.Qualifier
+import kotlin.reflect.KClass
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Qualifier
+annotation class ScreenModelKey(val screenModel: KClass)
diff --git a/core/common/src/main/java/ru/aleshin/core/common/extensions/Common.kt b/core/common/src/main/java/ru/aleshin/core/common/extensions/Common.kt
new file mode 100644
index 0000000..e86ad17
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/extensions/Common.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.extensions
+
+import java.lang.Math.abs
+import java.util.*
+import kotlin.random.Random
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+fun List.equalsIgnoreOrder(other: List) =
+ this.size == other.size && this.toSet() == other.toSet()
+
+fun Int?.toStringOrEmpty() = this?.toString() ?: ""
+
+fun generateUniqueKey() = abs(Random(Calendar.getInstance().timeInMillis).nextLong())
+
+fun generateSixDigitCode(): String {
+ return abs(Random(Calendar.getInstance().timeInMillis).nextInt()).toString().substring(IntRange(0, 5))
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/extensions/Compose.kt b/core/common/src/main/java/ru/aleshin/core/common/extensions/Compose.kt
new file mode 100644
index 0000000..1f66b1d
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/extensions/Compose.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.extensions
+
+import android.graphics.BitmapFactory
+import android.media.MediaMetadataRetriever
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.*
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun LazyListState.isScrollingUp(): Boolean {
+ var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
+ var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
+ return remember {
+ derivedStateOf {
+ if (previousIndex != firstVisibleItemIndex) {
+ previousIndex > firstVisibleItemIndex
+ } else {
+ previousScrollOffset >= firstVisibleItemScrollOffset
+ }.also {
+ previousIndex = firstVisibleItemIndex
+ previousScrollOffset = firstVisibleItemScrollOffset
+ }
+ }
+ }.value
+}
+
+@Composable
+fun String.mapAudioPathToPreview(): ImageBitmap? {
+ val metadataRetriever = remember { MediaMetadataRetriever() }
+ val bitmap = try {
+ metadataRetriever.setDataSource(this)
+ val embedPic = metadataRetriever.embeddedPicture
+ BitmapFactory.decodeByteArray(embedPic, 0, embedPic!!.size)
+ } catch (e: Exception) {
+ e.printStackTrace().let { null }
+ }
+ return bitmap?.asImageBitmap()
+}
+
+@Composable
+fun String.mapVideoPathToPreview(): ImageBitmap? {
+ val metadataRetriever = remember { MediaMetadataRetriever() }
+ val bitmap = try {
+ metadataRetriever.setDataSource(this)
+ metadataRetriever.getFrameAtTime(5, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
+ } catch (e: Exception) {
+ e.printStackTrace().let { null }
+ }
+ return bitmap?.asImageBitmap()
+}
+
+
diff --git a/core/common/src/main/java/ru/aleshin/core/common/extensions/Context.kt b/core/common/src/main/java/ru/aleshin/core/common/extensions/Context.kt
new file mode 100644
index 0000000..b6c9785
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/extensions/Context.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.extensions
+
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.core.content.ContextCompat
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+fun Context.isAllowPermission(permission: String): Boolean {
+ return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
+}
+
+fun Context.fetchLocale() = resources.configuration.locales.get(0)
+
+fun Context.fetchCurrentLanguage() = fetchLocale().language
diff --git a/core/common/src/main/java/ru/aleshin/core/common/extensions/Date.kt b/core/common/src/main/java/ru/aleshin/core/common/extensions/Date.kt
new file mode 100644
index 0000000..10fcd28
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/extensions/Date.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.extensions
+
+import ru.aleshin.core.common.functional.Constants
+import ru.aleshin.core.common.functional.TimeRange
+import java.util.*
+import kotlin.math.ceil
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+fun Date.shiftDay(amount: Int, locale: Locale = Locale.getDefault()): Date {
+ val calendar = Calendar.getInstance(locale).also {
+ it.time = this@shiftDay
+ startThisDay()
+ it.add(Calendar.DAY_OF_YEAR, amount)
+ }
+ return calendar.time
+}
+
+fun Date.shiftMinutes(amount: Int, locale: Locale = Locale.getDefault()): Date {
+ val calendar = Calendar.getInstance(locale).also {
+ it.time = this@shiftMinutes
+ it.add(Calendar.MINUTE, amount)
+ }
+ return calendar.time
+}
+
+fun Date.shiftMillis(amount: Int, locale: Locale = Locale.getDefault()): Date {
+ val calendar = Calendar.getInstance(locale).also {
+ it.time = this@shiftMillis
+ it.add(Calendar.MILLISECOND, amount)
+ }
+ return calendar.time
+}
+
+fun Date.isCurrentDay(date: Date): Boolean {
+ val currentDate = Calendar.getInstance().apply { time = date }.get(Calendar.DAY_OF_YEAR)
+ val compareDate =
+ Calendar.getInstance().apply { time = this@isCurrentDay }.get(Calendar.DAY_OF_YEAR)
+
+ return currentDate == compareDate
+}
+
+fun Date.compareByHoursAndMinutes(compareDate: Date): Boolean {
+ val firstCalendar = Calendar.getInstance().apply { time = this@compareByHoursAndMinutes }
+ val secondCalendar = Calendar.getInstance().apply { time = compareDate }
+ val hoursEquals = firstCalendar.get(Calendar.HOUR_OF_DAY) == secondCalendar.get(Calendar.HOUR_OF_DAY)
+ val minutesEquals = firstCalendar.get(Calendar.MINUTE) == secondCalendar.get(Calendar.MINUTE)
+
+ return hoursEquals && minutesEquals
+}
+
+fun Date.startThisDay(): Date {
+ val calendar = Calendar.getInstance()
+ calendar.time = this
+ return calendar.setStartDay().time
+}
+
+fun Date.endThisDay(): Date {
+ val calendar = Calendar.getInstance()
+ calendar.time = this
+ return calendar.setEndDay().time
+}
+
+fun Calendar.setStartDay() = this.apply {
+ set(Calendar.HOUR_OF_DAY, 0)
+ set(Calendar.MINUTE, 0)
+ set(Calendar.SECOND, 0)
+ set(Calendar.MILLISECOND, 0)
+}
+
+fun Calendar.setEndDay() = this.apply {
+ set(Calendar.HOUR_OF_DAY, 23)
+ set(Calendar.MINUTE, 59)
+ set(Calendar.SECOND, 59)
+ set(Calendar.MILLISECOND, 59)
+}
+
+fun Calendar.setHoursAndMinutes(hours: Int, minutes: Int) = this.apply {
+ set(Calendar.HOUR_OF_DAY, hours)
+ set(Calendar.MINUTE, minutes)
+}
+
+fun Date.changeDay(date: Date): Date {
+ val changedDateCalendar = Calendar.getInstance().apply {
+ time = this@changeDay
+ }
+ val newDateCalendar = Calendar.getInstance().apply {
+ time = date.startThisDay()
+ set(Calendar.HOUR_OF_DAY, changedDateCalendar.get(Calendar.HOUR_OF_DAY))
+ set(Calendar.MINUTE, changedDateCalendar.get(Calendar.MINUTE))
+ set(Calendar.SECOND, changedDateCalendar.get(Calendar.SECOND))
+ set(Calendar.MILLISECOND, changedDateCalendar.get(Calendar.MILLISECOND))
+ }
+ return newDateCalendar.time
+}
+
+fun duration(start: Date, end: Date): Long {
+ return end.time - start.time
+}
+
+fun Date.isNotZeroDifference(end: Date): Boolean {
+ return duration(this, end) > 0L
+}
+
+fun duration(timeRange: TimeRange): Long {
+ return timeRange.to.time - timeRange.from.time
+}
+
+fun durationOrZero(start: Date?, end: Date?) = if (start != null && end != null) {
+ duration(start, end)
+} else {
+ Constants.Date.EMPTY_DURATION
+}
+
+fun Long?.mapToDateOrDefault(defualt: Date): Date {
+ val calendar = Calendar.getInstance().also {
+ it.timeInMillis = this ?: defualt.time
+ }
+ return calendar.time
+}
+
+fun Long.mapToDate(): Date {
+ val calendar = Calendar.getInstance().also {
+ it.timeInMillis = this
+ }
+ return calendar.time
+}
+
+fun Long.toSeconds(): Long {
+ return this / Constants.Date.MILLIS_IN_SECONDS
+}
+
+fun Long.toMinutes(): Long {
+ return toSeconds() / Constants.Date.SECONDS_IN_MINUTE
+}
+
+fun Long.toMinutesInHours(): Long {
+ val hours = toHorses()
+ val minutes = toMinutes()
+ return minutes - hours * Constants.Date.MINUTES_IN_HOUR
+}
+
+fun Long.toHorses(): Long {
+ return toMinutes() / Constants.Date.MINUTES_IN_HOUR
+}
+
+fun Int.minutesToMillis(): Long {
+ return this * Constants.Date.MILLIS_IN_MINUTE
+}
+
+fun Int.hoursToMillis(): Long {
+ return this * Constants.Date.MILLIS_IN_HOUR
+}
+
+fun Long.toMinutesOrHoursString(minutesSymbol: String, hoursSymbol: String): String {
+ val minutes = this.toMinutes()
+ val hours = this.toHorses()
+
+ return if (minutes == 0L) {
+ Constants.Date.minutesFormat.format("1", minutesSymbol)
+ } else if (minutes in 1L..59L) {
+ Constants.Date.minutesFormat.format(minutes.toString(), minutesSymbol)
+ } else if (minutes > 59L && (minutes % 60L) != 0L) {
+ Constants.Date.hoursAndMinutesFormat.format(
+ hours.toString(),
+ hoursSymbol,
+ toMinutesInHours().toString(),
+ minutesSymbol,
+ )
+ } else {
+ Constants.Date.hoursFormat.format(hours.toString(), hoursSymbol)
+ }
+}
+
+fun Long.toMinutesAndHoursString(minutesSymbol: String, hoursSymbol: String): String {
+ val minutes = this.toMinutes()
+ val hours = this.toHorses()
+
+ return Constants.Date.hoursAndMinutesFormat.format(
+ hours.toString(),
+ hoursSymbol,
+ (minutes - hours * Constants.Date.MINUTES_IN_HOUR).toString(),
+ minutesSymbol,
+ )
+}
+
+
+fun Long.toSecondsAndMinutesString(): String {
+ val seconds = this.toSeconds()
+ val minutes = this.toMinutes()
+
+ return Constants.Date.secondsAndMinutesFormat.format(
+ minutes.toString(),
+ (seconds - minutes * Constants.Date.SECONDS_IN_MINUTE).let { if (it < 1) 0 else it }.toString(),
+ )
+}
+
+fun Date.setZeroSecond(): Date {
+ val calendar = Calendar.getInstance().apply {
+ time = this@setZeroSecond
+ set(Calendar.SECOND, 0)
+ }
+
+ return calendar.time
+}
+
+fun TimeRange.isIncludeTime(time: Date): Boolean {
+ return time >= this.from && time <= this.to
+}
+
+fun TimeRange.toDaysTitle(): String {
+ val calendar = Calendar.getInstance()
+ val dayStart = calendar.apply { time = from }.get(Calendar.DAY_OF_MONTH)
+ val dayEnd = calendar.apply { time = to }.get(Calendar.DAY_OF_MONTH)
+ return "$dayStart-$dayEnd"
+}
+
+fun TimeRange.toMonthTitle(): String {
+ val calendar = Calendar.getInstance()
+ val monthStart = calendar.apply { time = from }.get(Calendar.MONTH) + 1
+ val monthEnd = calendar.apply { time = to }.get(Calendar.MONTH) + 1
+ return "$monthStart-$monthEnd"
+}
+
+fun countWeeksByDays(days: Int): Int {
+ return ceil(days.toDouble() / Constants.Date.DAYS_IN_WEEK).toInt()
+}
+
+fun countMonthByDays(days: Int): Int {
+ return ceil(days.toDouble() / Constants.Date.DAYS_IN_MONTH).toInt()
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/Constants.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/Constants.kt
new file mode 100644
index 0000000..7f3a32d
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/Constants.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.functional
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+object Constants {
+
+ object Notification {
+ const val CHANNEL_ID = "mixPlayerAlarmChannel"
+ const val CHANNEL_NAME = "MixPlayer Service"
+ const val FOREGROUND_NOTIFY_ID = 209567821
+ const val ACTION_PLAY_PAUSE = "PLAYER_ACTION_PLAY_PAUSE"
+ const val ACTION_NEXT = "PLAYER_ACTION_NEXT_TRACK"
+ const val ACTION_PREVIOUS = "PLAYER_ACTION_PREVIOUS_TRACK"
+ }
+
+ object Alarm {
+ const val ALARM_NOTIFICATION_ACTION = "ru.aleshin.ALARM_NOTIFICATION_ACTION"
+ const val NOTIFICATION_ICON = "ALARM_DATA_ICON"
+ const val APP_ICON = "ALARM_DATA_APP_ICON"
+ }
+
+ object Delay {
+ const val SLIDER_POSITION_UPDATE = 500L
+ const val AUTH = 500L
+ const val LOAD_ANIMATION = 200L
+ const val LOAD_ANIMATION_LONG = 400L
+ const val SPLASH = 2000L
+ const val SPLASH_TEXT = 800L
+ const val CHECK_STATUS = 5000L
+ }
+
+ object Date {
+ const val DAY = 1
+ const val DAYS_IN_WEEK = 7
+ const val DAYS_IN_MONTH = 31
+ const val DAYS_IN_HALF_YEAR = 183
+ const val DAYS_IN_YEAR = 365
+
+ const val EMPTY_DURATION = 0L
+ const val MILLIS_IN_SECONDS = 1000L
+ const val MILLIS_IN_MINUTE = 60000L
+ const val MILLIS_IN_HOUR = 3600000L
+ const val SECONDS_IN_MINUTE = 60L
+ const val MINUTES_IN_MILLIS = 60000L
+ const val MINUTES_IN_HOUR = 60L
+ const val HOURS_IN_DAY = 24L
+
+ const val minutesFormat = "%s%s"
+ const val hoursFormat = "%s%s"
+ const val hoursAndMinutesFormat = "%s%s %s%s"
+ const val secondsAndMinutesFormat = "%s:%s"
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/DomainFailures.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/DomainFailures.kt
new file mode 100644
index 0000000..f1053d4
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/DomainFailures.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.functional
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface DomainFailures
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/Either.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/Either.kt
new file mode 100644
index 0000000..01d77cf
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/Either.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.functional
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+sealed class Either {
+
+ data class Left(val data: L) : Either()
+
+ data class Right(val data: R) : Either()
+
+ val isLeft = this is Left
+
+ val isRight = this is Right
+}
+
+typealias DomainResult = Either
+
+typealias UnitDomainResult = Either
+
+fun Either.rightOrElse(elseValue: R): R = when (this) {
+ is Either.Left -> elseValue
+ is Either.Right -> this.data
+}
+
+fun Either.leftOrElse(elseValue: L): L = when (this) {
+ is Either.Left -> this.data
+ is Either.Right -> elseValue
+}
+
+suspend fun Either.handle(
+ onLeftAction: suspend (L) -> Unit = {},
+ onRightAction: suspend (R) -> Unit = {},
+) = when (this) {
+ is Either.Left -> onLeftAction.invoke(this.data)
+ is Either.Right -> onRightAction.invoke(this.data)
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/Mapper.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/Mapper.kt
new file mode 100644
index 0000000..8689654
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/Mapper.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.functional
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface Mapper {
+ fun map(input: I): O
+}
+
+interface ParameterizedMapper {
+ fun map(input: I, parameter: P): O
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/MediaCommand.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/MediaCommand.kt
new file mode 100644
index 0000000..0f67775
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/MediaCommand.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.functional
+
+import ru.aleshin.core.common.functional.audio.AudioInfoUi
+import ru.aleshin.core.common.functional.audio.AudioPlayListType
+import ru.aleshin.core.common.functional.audio.AudioPlayListUi
+
+/**
+ * @author Stanislav Aleshin on 14.07.2023.
+ */
+sealed class MediaCommand {
+ object PlayOrPause : MediaCommand()
+ data class SeekTo(val value: Int) : MediaCommand()
+ data class ChangeVolume(val value: Float) : MediaCommand()
+ data class SelectAudio(val audio: AudioInfoUi, val type: AudioPlayListType) : MediaCommand()
+}
\ No newline at end of file
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/ResponseResult.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/ResponseResult.kt
new file mode 100644
index 0000000..dfa89bb
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/ResponseResult.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.functional
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+sealed class ResponseResult {
+
+ abstract fun map(dataTransform: (T) -> O): ResponseResult
+
+ sealed class Success : ResponseResult() {
+
+ abstract val data: T?
+
+ abstract val code: Int
+
+ data class Data(override val data: T, override val code: Int) : Success() {
+ override fun map(dataTransform: (T) -> O) = Data(dataTransform.invoke(data), code)
+ }
+
+ data class Empty(override val data: T?, override val code: Int) : Success() {
+ override fun map(dataTransform: (T) -> O) = Empty(null, code)
+ }
+ }
+
+ data class Error(val throwable: Throwable) : ResponseResult() {
+ override fun map(dataTransform: (Nothing) -> O) = this
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/TimeFormat.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/TimeFormat.kt
new file mode 100644
index 0000000..02ae414
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/TimeFormat.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.functional
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+enum class TimeFormat {
+ PM, AM
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/TimeRange.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/TimeRange.kt
new file mode 100644
index 0000000..7452064
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/TimeRange.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.functional
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import java.util.*
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Parcelize
+data class TimeRange(val from: Date, val to: Date) : Parcelable
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/audio/AudioInfoUi.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/AudioInfoUi.kt
new file mode 100644
index 0000000..1c376fd
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/AudioInfoUi.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.functional.audio
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import java.util.Date
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+@Parcelize
+data class AudioInfoUi(
+ val id: Long,
+ val path: String,
+ val title: String,
+ val artist: String?,
+ val album: String?,
+ val imagePath: String?,
+ val duration: Long,
+ val date: Date,
+) : Parcelable
\ No newline at end of file
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/audio/AudioPlayListType.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/AudioPlayListType.kt
new file mode 100644
index 0000000..276ca08
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/AudioPlayListType.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.functional.audio
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * @author Stanislav Aleshin on 13.07.2023.
+ */
+@Parcelize
+enum class AudioPlayListType : Parcelable {
+ SYSTEM, APP, OTHER
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/audio/AudioPlayListUi.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/AudioPlayListUi.kt
new file mode 100644
index 0000000..4b3b71a
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/AudioPlayListUi.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.functional.audio
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * @author Stanislav Aleshin on 14.07.2023.
+ */
+@Parcelize
+data class AudioPlayListUi(
+ val listType: AudioPlayListType,
+ val audioList: List
+) : Parcelable
\ No newline at end of file
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/audio/PlaybackInfoUi.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/PlaybackInfoUi.kt
new file mode 100644
index 0000000..e2fcc74
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/PlaybackInfoUi.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.functional.audio
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * @author Stanislav Aleshin on 14.07.2023.
+ */
+@Parcelize
+data class PlaybackInfoUi(
+ val currentAudio: AudioInfoUi? = null,
+ val isPlay: Boolean = false,
+ val position: Int = 0,
+ val volume: Float = 1f,
+ val isComplete: Boolean = false,
+) : Parcelable
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/audio/PlayerError.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/PlayerError.kt
new file mode 100644
index 0000000..3d213c7
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/PlayerError.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.functional.audio
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+@Parcelize
+enum class PlayerError : Parcelable {
+ DATA_SOURCE, OTHER, AUDIO_FOCUS
+}
\ No newline at end of file
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/audio/PlayerInfo.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/PlayerInfo.kt
new file mode 100644
index 0000000..5e1500a
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/audio/PlayerInfo.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.functional.audio
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * @author Stanislav Aleshin on 14.07.2023.
+ */
+@Parcelize
+data class PlayerInfo(
+ val playback: PlaybackInfoUi = PlaybackInfoUi(),
+ val playListType: AudioPlayListType = AudioPlayListType.SYSTEM,
+) : Parcelable
\ No newline at end of file
diff --git a/core/common/src/main/java/ru/aleshin/core/common/functional/video/VideoInfoUi.kt b/core/common/src/main/java/ru/aleshin/core/common/functional/video/VideoInfoUi.kt
new file mode 100644
index 0000000..2cdb9fa
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/functional/video/VideoInfoUi.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.functional.video
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+@Parcelize
+data class VideoInfoUi(
+ val id: Long,
+ val path: String,
+ val title: String,
+ val imagePath: String?,
+ val duration: Long,
+) : Parcelable
\ No newline at end of file
diff --git a/core/common/src/main/java/ru/aleshin/core/common/handlers/ErrorHandler.kt b/core/common/src/main/java/ru/aleshin/core/common/handlers/ErrorHandler.kt
new file mode 100644
index 0000000..d4fd5fb
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/handlers/ErrorHandler.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.handlers
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface ErrorHandler {
+ fun handle(throwable: Throwable): E
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/managers/AudioManagerController.kt b/core/common/src/main/java/ru/aleshin/core/common/managers/AudioManagerController.kt
new file mode 100644
index 0000000..c704f3d
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/managers/AudioManagerController.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.managers
+
+import android.media.AudioAttributes
+import android.media.AudioFocusRequest
+import android.media.AudioManager
+import android.media.AudioManager.OnAudioFocusChangeListener
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+/**
+ * @author Stanislav Aleshin on 13.07.2023.
+ */
+interface AudioManagerController {
+
+ fun captureAudioFocus(listener: OnAudioFocusChangeListener): Boolean
+
+ fun freeAudioFocus(listener: OnAudioFocusChangeListener): Boolean
+
+ class Base(
+ private val audioManager: AudioManager,
+ private val audioAttributes: AudioAttributes,
+ ) : AudioManagerController {
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private val focusRequesterBuilder = AudioFocusRequest.Builder(
+ AudioManager.AUDIOFOCUS_GAIN
+ ).apply {
+ setAudioAttributes(audioAttributes)
+ setAcceptsDelayedFocusGain(true)
+ setWillPauseWhenDucked(true)
+ }
+
+ override fun captureAudioFocus(listener: OnAudioFocusChangeListener): Boolean {
+ val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val focusRequest = focusRequesterBuilder.setOnAudioFocusChangeListener(listener)
+ audioManager.requestAudioFocus(focusRequest.build())
+ } else {
+ audioManager.requestAudioFocus(
+ listener,
+ AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN
+ )
+ }
+ return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
+ }
+
+ override fun freeAudioFocus(listener: OnAudioFocusChangeListener): Boolean {
+ val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val focusRequest = focusRequesterBuilder.setOnAudioFocusChangeListener(listener)
+ audioManager.abandonAudioFocusRequest(focusRequest.build())
+ } else {
+ audioManager.abandonAudioFocus(listener)
+ }
+ return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/core/common/src/main/java/ru/aleshin/core/common/managers/AudioStoreUtils.kt b/core/common/src/main/java/ru/aleshin/core/common/managers/AudioStoreUtils.kt
new file mode 100644
index 0000000..d8256a5
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/managers/AudioStoreUtils.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.managers
+
+import android.database.Cursor
+import android.provider.MediaStore.Audio.Media
+import ru.aleshin.core.common.extensions.mapToDate
+import java.util.Date
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+object AudioStoreUtils {
+
+ fun fetchId(cursor: Cursor): Long? = cursor.getColumnIndex(Media._ID).let {
+ if (it != -1) cursor.getLong(it) else null
+ }
+
+ fun fetchPath(cursor: Cursor): String? = cursor.getColumnIndex(Media.DATA).let {
+ if (it != -1) cursor.getString(it) else null
+ }
+
+ fun fetchTitle(cursor: Cursor): String? = cursor.getColumnIndex(Media.TITLE).let {
+ if (it != -1) cursor.getString(it) else null
+ }
+
+ fun fetchArtist(cursor: Cursor): String? = cursor.getColumnIndex(Media.ARTIST).let {
+ when (it != -1) {
+ true -> {
+ val artist = cursor.getString(it)
+ if (artist != "") artist else null
+ }
+ false -> null
+ }
+ }
+
+ fun fetchAlbum(cursor: Cursor): String? = cursor.getColumnIndex(Media.ALBUM).let {
+ when (it != -1) {
+ true -> {
+ val album = cursor.getString(it)
+ if (album != "") album else null
+ }
+ false -> null
+ }
+ }
+
+ fun fetchAlbumId(cursor: Cursor): Long? = cursor.getColumnIndex(Media.ALBUM_ID).let {
+ when (it != -1) {
+ true -> cursor.getLong(it)
+ false -> null
+ }
+ }
+
+ fun fetchDuration(cursor: Cursor): Long? = cursor.getColumnIndex(Media.DURATION).let {
+ if (it != -1) cursor.getLong(it) else null
+ }
+
+ fun fetchDate(cursor: Cursor): Date? = cursor.getColumnIndex(Media.DATE_EXPIRES).let {
+ when (it != -1) {
+ true -> cursor.getLong(it).mapToDate()
+ false -> null
+ }
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/managers/BitmapUtils.kt b/core/common/src/main/java/ru/aleshin/core/common/managers/BitmapUtils.kt
new file mode 100644
index 0000000..1b27835
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/managers/BitmapUtils.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.managers
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import androidx.compose.ui.graphics.asImageBitmap
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.nio.ByteBuffer
+
+/**
+ * Created on 14.07.2023.
+ */
+object BitmapUtils {
+
+ fun convertBitmapToByteArray(bitmap: Bitmap): ByteArray {
+ var baos: ByteArrayOutputStream? = null
+ return try {
+ baos = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 60, baos)
+ baos.toByteArray()
+ } finally {
+ if (baos != null) {
+ try {
+ baos.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ }
+ }
+
+ fun convertBitmapToByteArrayUncompressed(bitmap: Bitmap): ByteArray {
+ val byteBuffer = ByteBuffer.allocate(bitmap.byteCount)
+ bitmap.copyPixelsToBuffer(byteBuffer)
+ byteBuffer.rewind()
+ return byteBuffer.array()
+ }
+
+ fun convertCompressedByteArrayToBitmap(src: ByteArray): Bitmap {
+ return BitmapFactory.decodeByteArray(src, 0, src.size)
+ }
+}
+
+fun ByteArray.toImageBitmap() = BitmapUtils.convertCompressedByteArrayToBitmap(this).asImageBitmap()
\ No newline at end of file
diff --git a/core/common/src/main/java/ru/aleshin/core/common/managers/CoroutineManager.kt b/core/common/src/main/java/ru/aleshin/core/common/managers/CoroutineManager.kt
new file mode 100644
index 0000000..459f4d8
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/managers/CoroutineManager.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.managers
+
+import kotlinx.coroutines.* // ktlint-disable no-wildcard-imports
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.flowOn
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface CoroutineManager {
+
+ suspend fun Flow.collectOnBackground(collector: FlowCollector)
+
+ fun runOnBackground(scope: CoroutineScope, block: CoroutineBlock): Job
+
+ fun runOnUi(scope: CoroutineScope, block: CoroutineBlock): Job
+
+ suspend fun changeFlow(coroutineFlow: CoroutineFlow, block: CoroutineBlock)
+
+ abstract class Abstract(
+ private val backgroundDispatcher: CoroutineDispatcher,
+ private val uiDispatcher: CoroutineDispatcher,
+ ) : CoroutineManager {
+
+ override suspend fun Flow.collectOnBackground(collector: FlowCollector) {
+ this.flowOn(backgroundDispatcher).collect(collector)
+ }
+
+ override fun runOnBackground(scope: CoroutineScope, block: CoroutineBlock): Job {
+ return scope.launch(context = backgroundDispatcher, block = block)
+ }
+
+ override fun runOnUi(scope: CoroutineScope, block: CoroutineBlock): Job {
+ return scope.launch(context = uiDispatcher, block = block)
+ }
+
+ override suspend fun changeFlow(coroutineFlow: CoroutineFlow, block: CoroutineBlock) {
+ val dispatcher = when (coroutineFlow) {
+ CoroutineFlow.BACKGROUND -> backgroundDispatcher
+ CoroutineFlow.UI -> uiDispatcher
+ }
+ withContext(context = dispatcher, block = block)
+ }
+ }
+
+ class Base @Inject constructor() : Abstract(
+ backgroundDispatcher = Dispatchers.IO,
+ uiDispatcher = Dispatchers.Main,
+ )
+}
+
+typealias CoroutineBlock = suspend CoroutineScope.() -> Unit
+
+enum class CoroutineFlow {
+ BACKGROUND, UI
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/managers/DateManager.kt b/core/common/src/main/java/ru/aleshin/core/common/managers/DateManager.kt
new file mode 100644
index 0000000..6c4d049
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/managers/DateManager.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.managers
+
+import ru.aleshin.core.common.extensions.setEndDay
+import ru.aleshin.core.common.extensions.setStartDay
+import ru.aleshin.core.common.extensions.toMinutes
+import java.util.*
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface DateManager {
+
+ fun fetchCurrentDate(): Date
+ fun fetchBeginningCurrentDay(): Date
+ fun fetchEndCurrentDay(): Date
+ fun calculateLeftTime(endTime: Date): Long
+ fun calculateProgress(startTime: Date, endTime: Date): Float
+
+ class Base @Inject constructor() : DateManager {
+
+ override fun fetchCurrentDate() = checkNotNull(Calendar.getInstance().time)
+
+ override fun fetchBeginningCurrentDay(): Date {
+ val currentCalendar = Calendar.getInstance()
+ return currentCalendar.setStartDay().time
+ }
+
+ override fun fetchEndCurrentDay(): Date {
+ val currentCalendar = Calendar.getInstance()
+ return currentCalendar.setEndDay().time
+ }
+
+ override fun calculateLeftTime(endTime: Date): Long {
+ val currentDate = fetchCurrentDate()
+ return endTime.time - currentDate.time
+ }
+
+ override fun calculateProgress(startTime: Date, endTime: Date): Float {
+ val currentTime = fetchCurrentDate().time
+ val pastTime = ((currentTime - startTime.time).toMinutes()).toFloat()
+ val duration = ((endTime.time - startTime.time).toMinutes()).toFloat()
+ val progress = pastTime / duration
+
+ return if (progress < 0f) 0f else if (progress > 1f) 1f else progress
+ }
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/managers/DrawerManager.kt b/core/common/src/main/java/ru/aleshin/core/common/managers/DrawerManager.kt
new file mode 100644
index 0000000..efa76f6
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/managers/DrawerManager.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.managers
+
+import androidx.compose.material3.DrawerState
+import androidx.compose.material3.DrawerValue
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.staticCompositionLocalOf
+import kotlinx.coroutines.flow.MutableStateFlow
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface DrawerManager {
+
+ val drawerValue: State
+
+ val selectedItem: MutableStateFlow
+
+ suspend fun openDrawer()
+
+ suspend fun closeDrawer()
+
+ class Base @Inject constructor(internal val drawerState: DrawerState) : DrawerManager {
+
+ override val drawerValue = derivedStateOf { drawerState.currentValue }
+
+ override val selectedItem = MutableStateFlow(0)
+
+ override suspend fun openDrawer() {
+ drawerState.open()
+ }
+
+ override suspend fun closeDrawer() {
+ drawerState.close()
+ }
+
+ companion object {
+ fun Saver(drawerState: DrawerState) = Saver(
+ save = { null },
+ restore = { Base(drawerState) },
+ )
+ }
+ }
+}
+
+val LocalDrawerManager = staticCompositionLocalOf { null }
+
+@Composable
+fun rememberDrawerManager(
+ drawerState: DrawerState,
+): DrawerManager {
+ return rememberSaveable(saver = DrawerManager.Base.Saver(drawerState)) {
+ DrawerManager.Base(drawerState)
+ }
+}
+
+interface DrawerItem {
+ val icon: Int @Composable get
+ val title: String @Composable get
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/managers/VideoStoreUtils.kt b/core/common/src/main/java/ru/aleshin/core/common/managers/VideoStoreUtils.kt
new file mode 100644
index 0000000..6f028d5
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/managers/VideoStoreUtils.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.managers
+
+import android.database.Cursor
+import android.provider.MediaStore.Video.Media
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+object VideoStoreUtils {
+
+ fun fetchId(cursor: Cursor): Long? = cursor.getColumnIndex(Media._ID).let {
+ if (it != -1) cursor.getLong(it) else null
+ }
+
+ fun fetchPath(cursor: Cursor): String? = cursor.getColumnIndex(Media.DATA).let {
+ if (it != -1) cursor.getString(it) else null
+ }
+
+ fun fetchTitle(cursor: Cursor): String? = cursor.getColumnIndex(Media.TITLE).let {
+ if (it != -1) cursor.getString(it) else null
+ }
+
+ fun fetchDuration(cursor: Cursor): Long? = cursor.getColumnIndex(Media.DURATION).let {
+ if (it != -1) cursor.getLong(it) else null
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/navigation/Command.kt b/core/common/src/main/java/ru/aleshin/core/common/navigation/Command.kt
new file mode 100644
index 0000000..0052d7a
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/navigation/Command.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.navigation
+
+import cafe.adriel.voyager.core.screen.Screen
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+sealed class Command {
+ data class Forward(val screen: Screen) : Command()
+ data class Replace(val screen: Screen) : Command()
+ data class ReplaceAll(val screen: Screen) : Command()
+ object Back : Command()
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/navigation/CommandBuffer.kt b/core/common/src/main/java/ru/aleshin/core/common/navigation/CommandBuffer.kt
new file mode 100644
index 0000000..a59db51
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/navigation/CommandBuffer.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.navigation
+
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface CommandBuffer : CommandListenerManager {
+
+ fun sendCommand(command: Command)
+
+ class Base @Inject constructor() : CommandBuffer {
+
+ private val commandBuffer = mutableListOf()
+ private var commandListener: CommandListener? = null
+
+ override fun sendCommand(command: Command) {
+ commandListener?.invoke(command) ?: commandBuffer.add(command)
+ }
+
+ override fun setListener(listener: CommandListener) {
+ this.commandListener = listener
+
+ commandBuffer.forEach { listener.invoke(it) }
+ commandBuffer.clear()
+ }
+
+ override fun removeListener() {
+ commandListener = null
+ }
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/navigation/CommandListenerManager.kt b/core/common/src/main/java/ru/aleshin/core/common/navigation/CommandListenerManager.kt
new file mode 100644
index 0000000..f57d803
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/navigation/CommandListenerManager.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.navigation
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface CommandListenerManager {
+ fun setListener(listener: CommandListener)
+ fun removeListener()
+}
+
+typealias CommandListener = (Command) -> Unit
diff --git a/core/common/src/main/java/ru/aleshin/core/common/navigation/NavigationProcessor.kt b/core/common/src/main/java/ru/aleshin/core/common/navigation/NavigationProcessor.kt
new file mode 100644
index 0000000..85831cc
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/navigation/NavigationProcessor.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.navigation
+
+import cafe.adriel.voyager.navigator.Navigator
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface NavigationProcessor {
+
+ fun navigate(command: Command, navigator: Navigator)
+
+ class Base @Inject constructor() : NavigationProcessor {
+ override fun navigate(command: Command, navigator: Navigator) {
+ with(navigator) {
+ when (command) {
+ is Command.Forward -> push(command.screen)
+ is Command.Replace -> replace(command.screen)
+ is Command.ReplaceAll -> replaceAll(command.screen)
+ is Command.Back -> pop()
+ }
+ }
+ }
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/navigation/Router.kt b/core/common/src/main/java/ru/aleshin/core/common/navigation/Router.kt
new file mode 100644
index 0000000..cdf3ff5
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/navigation/Router.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.navigation
+
+import cafe.adriel.voyager.core.screen.Screen
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface Router {
+
+ fun navigateTo(screen: Screen)
+
+ fun replaceTo(screen: Screen, isAll: Boolean = false)
+
+ fun navigateBack()
+
+ abstract class Abstract constructor(private val commandBuffer: CommandBuffer) : Router {
+
+ override fun navigateTo(screen: Screen) {
+ commandBuffer.sendCommand(Command.Forward(screen))
+ }
+
+ override fun replaceTo(screen: Screen, isAll: Boolean) {
+ val command = if (isAll) Command.ReplaceAll(screen) else Command.Replace(screen)
+ commandBuffer.sendCommand(command)
+ }
+
+ override fun navigateBack() {
+ commandBuffer.sendCommand(Command.Back)
+ }
+ }
+
+ class Base @Inject constructor(commandBuffer: CommandBuffer) : Router, Abstract(commandBuffer)
+}
+
+interface TabRouter {
+
+ fun showTab(screen: Screen)
+
+ class Base @Inject constructor(private val router: Router) : TabRouter {
+ override fun showTab(screen: Screen) {
+ router.replaceTo(screen = screen, isAll = true)
+ }
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/navigation/navigator/AppNavigator.kt b/core/common/src/main/java/ru/aleshin/core/common/navigation/navigator/AppNavigator.kt
new file mode 100644
index 0000000..29545e0
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/navigation/navigator/AppNavigator.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.navigation.navigator
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.CurrentScreen
+import cafe.adriel.voyager.navigator.Navigator
+import cafe.adriel.voyager.navigator.NavigatorContent
+import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
+import cafe.adriel.voyager.navigator.OnBackPressed
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun AppNavigator(
+ initialScreen: Screen = EmptyScreen,
+ navigatorManager: NavigatorManager,
+ onBackPressed: OnBackPressed = { true },
+ disposeBehavior: NavigatorDisposeBehavior = NavigatorDisposeBehavior(
+ disposeNestedNavigators = false,
+ disposeSteps = true
+ ),
+ content: NavigatorContent = { CurrentScreen() },
+) {
+ Navigator(
+ initialScreen,
+ onBackPressed = onBackPressed,
+ disposeBehavior = disposeBehavior,
+ ) { navigator ->
+ DisposableEffect(Unit) {
+ navigatorManager.attachNavigator(navigator)
+ onDispose { navigatorManager.detachNavigator() }
+ }
+ content.invoke(navigator)
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/navigation/navigator/NavigatorManager.kt b/core/common/src/main/java/ru/aleshin/core/common/navigation/navigator/NavigatorManager.kt
new file mode 100644
index 0000000..4c80883
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/navigation/navigator/NavigatorManager.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.navigation.navigator
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisallowComposableCalls
+import androidx.compose.runtime.remember
+import cafe.adriel.voyager.navigator.Navigator
+import ru.aleshin.core.common.navigation.CommandBuffer
+import ru.aleshin.core.common.navigation.NavigationProcessor
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface NavigatorManager {
+
+ fun attachNavigator(navigator: Navigator)
+
+ fun detachNavigator()
+
+ class Base @Inject constructor(
+ private val commandBuffer: CommandBuffer,
+ private val navigationProcessor: NavigationProcessor = NavigationProcessor.Base(),
+ ) : NavigatorManager {
+
+ private var navigator: Navigator? = null
+
+ override fun attachNavigator(navigator: Navigator) {
+ this.navigator = navigator
+
+ commandBuffer.setListener { command ->
+ navigationProcessor.navigate(command, checkNotNull(this.navigator))
+ }
+ }
+
+ override fun detachNavigator() {
+ commandBuffer.removeListener()
+ navigator = null
+ }
+ }
+}
+
+@Composable
+fun rememberNavigatorManager(
+ factory: @DisallowComposableCalls () -> T,
+): NavigatorManager {
+ return remember { factory.invoke() }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/navigation/navigator/TabNavigator.kt b/core/common/src/main/java/ru/aleshin/core/common/navigation/navigator/TabNavigator.kt
new file mode 100644
index 0000000..3476091
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/navigation/navigator/TabNavigator.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.navigation.navigator
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.Navigator
+import cafe.adriel.voyager.navigator.NavigatorContent
+import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
+import cafe.adriel.voyager.navigator.OnBackPressed
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun TabNavigator(
+ initScreen: Screen = EmptyScreen,
+ navigatorManager: NavigatorManager,
+ onBackPressed: OnBackPressed = { true },
+ content: NavigatorContent,
+) {
+ Navigator(
+ screen = initScreen,
+ disposeBehavior = NavigatorDisposeBehavior(
+ disposeNestedNavigators = false,
+ disposeSteps = false,
+ ),
+ onBackPressed = onBackPressed,
+ ) { navigator ->
+ DisposableEffect(Unit) {
+ navigatorManager.attachNavigator(navigator)
+ onDispose { navigatorManager.detachNavigator() }
+ }
+ content.invoke(navigator)
+ }
+}
+
+object EmptyScreen : Screen {
+ @Composable
+ override fun Content() = Unit
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/notifications/NotificationCreator.kt b/core/common/src/main/java/ru/aleshin/core/common/notifications/NotificationCreator.kt
new file mode 100644
index 0000000..f3c526d
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/notifications/NotificationCreator.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.notifications
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.graphics.Bitmap
+import android.media.RingtoneManager
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.core.app.NotificationCompat
+import ru.aleshin.core.common.notifications.parameters.*
+import ru.aleshin.core.common.notifications.parameters.NotificationDefaults
+import ru.aleshin.core.common.notifications.parameters.NotificationPriority
+import ru.aleshin.core.common.notifications.parameters.NotificationProgress
+import ru.aleshin.core.common.notifications.parameters.NotificationStyles
+import ru.aleshin.core.common.notifications.parameters.NotificationVisibility
+import java.lang.Exception
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface NotificationCreator {
+
+ fun createNotify(
+ channelId: String,
+ title: String,
+ text: String,
+ timeStamp: Long? = System.currentTimeMillis(),
+ smallIcon: Int,
+ largeIcon: Bitmap? = null,
+ visibility: NotificationVisibility = NotificationVisibility.PUBLIC,
+ priority: NotificationPriority = NotificationPriority.DEFAULT,
+ actions: List = emptyList(),
+ contentIntent: PendingIntent? = null,
+ notificationDefaults: NotificationDefaults = NotificationDefaults(),
+ autoCancel: Boolean = true,
+ ongoing: Boolean = false,
+ style: NotificationStyles? = null,
+ color: Int? = null,
+ progress: NotificationProgress? = null,
+ ): Notification
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ fun createNotifyChannel(
+ channelId: String,
+ channelName: String,
+ priority: NotificationPriority,
+ defaults: NotificationDefaults = NotificationDefaults(),
+ )
+
+ fun showNotify(notification: Notification, notifyId: Int)
+
+ class Base @Inject constructor(
+ private val context: Context,
+ ) : NotificationCreator {
+
+ private val notificationManager: NotificationManager
+ get() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ override fun createNotify(
+ channelId: String,
+ title: String,
+ text: String,
+ timeStamp: Long?,
+ smallIcon: Int,
+ largeIcon: Bitmap?,
+ visibility: NotificationVisibility,
+ priority: NotificationPriority,
+ actions: List,
+ contentIntent: PendingIntent?,
+ notificationDefaults: NotificationDefaults,
+ autoCancel: Boolean,
+ ongoing: Boolean,
+ style: NotificationStyles?,
+ color: Int?,
+ progress: NotificationProgress?,
+ ): Notification {
+ val builder = when (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ true -> NotificationCompat.Builder(context, channelId)
+ false -> NotificationCompat.Builder(context)
+ }
+ builder.apply {
+ setContentTitle(title)
+ setContentText(text)
+ setVisibility(visibility.visibility)
+ setPriority(priority.importance)
+ if (timeStamp == null) setShowWhen(false) else setWhen(timeStamp)
+ if (color != null) setColor(color)
+ setSmallIcon(smallIcon)
+ if (largeIcon != null) setLargeIcon(largeIcon)
+ if (contentIntent != null) setContentIntent(contentIntent)
+ setAutoCancel(autoCancel)
+ setOngoing(ongoing)
+ if (notificationDefaults.isVibrate) setDefaults(NotificationCompat.DEFAULT_VIBRATE)
+ if (notificationDefaults.isSound) {
+ setDefaults(NotificationCompat.DEFAULT_SOUND)
+ try {
+ val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
+ RingtoneManager.getRingtone(context, soundUri).play()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ if (notificationDefaults.isLights) setDefaults(NotificationCompat.DEFAULT_LIGHTS)
+ if (progress != null) with(progress) { setProgress(max, value, isIndeterminate) }
+ if (style != null) setStyle(style.style)
+ actions.forEach { addAction(it) }
+ }
+ return builder.build()
+ }
+
+ override fun showNotify(notification: Notification, notifyId: Int) {
+ notificationManager.notify(notifyId, notification)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun createNotifyChannel(
+ channelId: String,
+ channelName: String,
+ priority: NotificationPriority,
+ defaults: NotificationDefaults,
+ ) {
+ val channel = NotificationChannel(channelId, channelName, priority.importance).apply {
+ enableLights(defaults.isLights)
+ enableVibration(defaults.isVibrate)
+ vibrationPattern = longArrayOf(500, 500, 500)
+ }
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationDefaults.kt b/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationDefaults.kt
new file mode 100644
index 0000000..473cff1
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationDefaults.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.notifications.parameters
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+data class NotificationDefaults(
+ val isSound: Boolean = false,
+ val isVibrate: Boolean = false,
+ val isLights: Boolean = false,
+)
diff --git a/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationPriority.kt b/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationPriority.kt
new file mode 100644
index 0000000..dc711aa
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationPriority.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.notifications.parameters
+
+import android.app.NotificationManager
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+enum class NotificationPriority(val importance: Int) {
+ DEFAULT(NotificationManager.IMPORTANCE_DEFAULT),
+ MIN(NotificationManager.IMPORTANCE_MIN),
+ LOW(NotificationManager.IMPORTANCE_LOW),
+ HIGH(NotificationManager.IMPORTANCE_HIGH),
+ MAX(NotificationManager.IMPORTANCE_MAX),
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationProgress.kt b/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationProgress.kt
new file mode 100644
index 0000000..767078e
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationProgress.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.notifications.parameters
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+data class NotificationProgress(val value: Int, val max: Int, val isIndeterminate: Boolean)
diff --git a/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationStyles.kt b/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationStyles.kt
new file mode 100644
index 0000000..75a1e03
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationStyles.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.notifications.parameters
+
+import android.graphics.Bitmap
+import androidx.core.app.NotificationCompat
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+sealed class NotificationStyles {
+
+ abstract val style: NotificationCompat.Style
+
+ data class BigTextStyle(
+ val text: String,
+ val bigTitle: String? = null,
+ val summary: String? = null,
+ ) : NotificationStyles() {
+ override val style: NotificationCompat.Style
+ get() {
+ val style = NotificationCompat.BigTextStyle().bigText(text)
+ if (bigTitle != null) style.setBigContentTitle(bigTitle)
+ if (summary != null) style.setSummaryText(summary)
+ return style
+ }
+ }
+
+ data class BigPictureStyle(
+ val bitMap: Bitmap?,
+ val largeIcon: Bitmap? = null,
+ val bigTitle: String? = null,
+ val summary: String? = null,
+ ) : NotificationStyles() {
+ override val style: NotificationCompat.Style
+ get() {
+ val style = NotificationCompat.BigPictureStyle().bigPicture(bitMap)
+ if (largeIcon != null) style.bigLargeIcon(largeIcon)
+ if (bigTitle != null) style.setBigContentTitle(bigTitle)
+ if (summary != null) style.setSummaryText(summary)
+ return style
+ }
+ }
+
+ data class InboxStyle(
+ val lines: List,
+ val summary: String? = null,
+ ) : NotificationStyles() {
+ override val style: NotificationCompat.Style
+ get() {
+ val style = NotificationCompat.InboxStyle()
+ if (summary != null) style.setSummaryText(summary)
+ lines.forEach { line -> style.addLine(line) }
+ return style
+ }
+ }
+
+ data class Other(override val style: NotificationCompat.Style) : NotificationStyles()
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationVisibility.kt b/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationVisibility.kt
new file mode 100644
index 0000000..3ba3002
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/notifications/parameters/NotificationVisibility.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.notifications.parameters
+
+import androidx.core.app.NotificationCompat
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+enum class NotificationVisibility(val visibility: Int) {
+ PUBLIC(NotificationCompat.VISIBILITY_PUBLIC),
+ SECRET(NotificationCompat.VISIBILITY_SECRET),
+ PRIVATE(NotificationCompat.VISIBILITY_PRIVATE),
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/activity/BaseActivity.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/activity/BaseActivity.kt
new file mode 100644
index 0000000..ad3bfef
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/activity/BaseActivity.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.activity
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.runtime.Composable
+import androidx.lifecycle.ViewModelProvider
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+import ru.aleshin.core.common.platform.screenmodel.BaseViewModel
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+abstract class BaseActivity : ComponentActivity() {
+
+ protected val viewModel by lazy {
+ ViewModelProvider(this, fetchViewModelFactory())[fetchViewModelClass()]
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ initDI()
+ super.onCreate(savedInstanceState)
+ setContent { Content() }
+ }
+
+ open fun initDI() {}
+
+ @Composable
+ abstract fun Content()
+
+ abstract fun fetchViewModelFactory(): ViewModelProvider.Factory
+
+ abstract fun fetchViewModelClass(): Class>
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/communications/Communicator.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/communications/Communicator.kt
new file mode 100644
index 0000000..6ac958d
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/communications/Communicator.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.communications
+
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface Communicator {
+
+ suspend fun collect(collector: FlowCollector)
+
+ suspend fun read(): T
+
+ fun update(data: T)
+
+ abstract class AbstractStateFlow(defaultValue: T) : Communicator {
+
+ private val flow = MutableStateFlow(value = defaultValue)
+
+ override suspend fun collect(collector: FlowCollector) {
+ flow.collect(collector)
+ }
+
+ override suspend fun read(): T {
+ return flow.value
+ }
+
+ override fun update(data: T) {
+ flow.value = data
+ }
+ }
+
+ abstract class AbstractSharedFlow(
+ flowReplay: Int = 0,
+ flowBufferCapacity: Int = 0,
+ flowBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
+ ) : Communicator {
+
+ private val flow = MutableSharedFlow(
+ replay = flowReplay,
+ extraBufferCapacity = flowBufferCapacity,
+ onBufferOverflow = flowBufferOverflow,
+ )
+
+ override suspend fun collect(collector: FlowCollector) {
+ flow.collect(collector)
+ }
+
+ override suspend fun read(): T {
+ return flow.first()
+ }
+
+ override fun update(data: T) {
+ flow.tryEmit(data)
+ }
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/communications/state/StateCommunicator.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/communications/state/StateCommunicator.kt
new file mode 100644
index 0000000..78ebe42
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/communications/state/StateCommunicator.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.communications.state
+
+import ru.aleshin.core.common.platform.communications.Communicator
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+import ru.aleshin.core.common.platform.screenmodel.contract.EmptyUiEffect
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface StateCommunicator : Communicator {
+
+ abstract class Abstract(defaultState: S) : StateCommunicator,
+ Communicator.AbstractStateFlow(defaultState)
+}
+
+interface EffectCommunicator : Communicator {
+
+ abstract class Abstract : EffectCommunicator,
+ Communicator.AbstractSharedFlow(flowBufferCapacity = 1)
+
+ class Empty : Abstract()
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/communications/state/ViewStateCollect.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/communications/state/ViewStateCollect.kt
new file mode 100644
index 0000000..10047b2
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/communications/state/ViewStateCollect.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.communications.state
+
+import kotlinx.coroutines.flow.FlowCollector
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface ViewStateCollect {
+ suspend fun collectState(collector: FlowCollector)
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screen/RequestPermissions.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screen/RequestPermissions.kt
new file mode 100644
index 0000000..97adaa4
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screen/RequestPermissions.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screen
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun RequestPermissions(
+ permissionsList: Array,
+ onGrantedPermission: (String) -> Unit,
+ onDeniedPermissions: () -> Unit,
+) {
+ val permissionRequest = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions(),
+ ) { permissions ->
+ permissionsList.forEach {
+ if (permissions.getOrDefault(it, false)) {
+ onGrantedPermission.invoke(it)
+ return@rememberLauncherForActivityResult
+ }
+ }
+ onDeniedPermissions.invoke()
+ }
+
+ SideEffect {
+ permissionRequest.launch(permissionsList)
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screen/ScreenContent.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screen/ScreenContent.kt
new file mode 100644
index 0000000..97ef025
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screen/ScreenContent.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.platform.screen
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import ru.aleshin.core.common.platform.screenmodel.ContractProvider
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun > ScreenContent(
+ screenModel: CP,
+ initialState: S,
+ content: @Composable ScreenScope.(state: S) -> Unit,
+) {
+ LaunchedEffect(key1 = Unit) { screenModel.init() }
+ val screenScope = rememberScreenScope(
+ contractProvider = screenModel,
+ initialState = initialState,
+ )
+ content.invoke(screenScope, screenScope.fetchState())
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screen/ScreenScope.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screen/ScreenScope.kt
new file mode 100644
index 0000000..fafb53f
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screen/ScreenScope.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.platform.screen
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.SaverScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import kotlinx.coroutines.CoroutineScope
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+import ru.aleshin.core.common.platform.screenmodel.ContractProvider
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface ScreenScope {
+
+ fun dispatchEvent(event: E)
+
+ @Composable
+ fun fetchState(): S
+
+ @Composable
+ fun handleEffect(block: suspend CoroutineScope.(F) -> Unit)
+
+ class Base(
+ private val contractProvider: ContractProvider,
+ internal val initialState: S,
+ ) : ScreenScope {
+
+ override fun dispatchEvent(event: E) {
+ contractProvider.dispatchEvent(event)
+ }
+
+ @Composable
+ override fun fetchState(): S {
+ val state = rememberSaveable { mutableStateOf(initialState) }
+ LaunchedEffect(Unit) { contractProvider.collectState { state.value = it } }
+
+ return state.value
+ }
+
+ @Composable
+ override fun handleEffect(
+ block: suspend CoroutineScope.(F) -> Unit,
+ ) = LaunchedEffect(Unit) {
+ contractProvider.collectUiEffect { effect -> block(effect) }
+ }
+ }
+}
+
+@Composable
+fun rememberScreenScope(
+ contractProvider: ContractProvider,
+ initialState: S,
+): ScreenScope {
+ return rememberSaveable(saver = screenScopeSaver(contractProvider)) {
+ ScreenScope.Base(contractProvider, initialState)
+ }
+}
+
+private fun screenScopeSaver(
+ contractProvider: ContractProvider,
+): Saver, S> = object : Saver, S> {
+
+ override fun SaverScope.save(value: ScreenScope.Base): S {
+ return value.initialState
+ }
+
+ override fun restore(value: S): ScreenScope.Base {
+ return ScreenScope.Base(contractProvider, value)
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/Actor.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/Actor.kt
new file mode 100644
index 0000000..e6ab173
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/Actor.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel
+
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+import ru.aleshin.core.common.platform.screenmodel.work.WorkScope
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface Actor {
+ suspend fun WorkScope.handleEvent(event: E)
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/BaseScreenModel.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/BaseScreenModel.kt
new file mode 100644
index 0000000..f5ee4c0
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/BaseScreenModel.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.platform.screenmodel
+
+import cafe.adriel.voyager.core.model.ScreenModel
+import cafe.adriel.voyager.core.model.coroutineScope
+import kotlinx.coroutines.flow.FlowCollector
+import ru.aleshin.core.common.managers.CoroutineManager
+import ru.aleshin.core.common.platform.communications.state.EffectCommunicator
+import ru.aleshin.core.common.platform.communications.state.StateCommunicator
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+import ru.aleshin.core.common.platform.screenmodel.store.launchedStore
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+abstract class BaseScreenModel(
+ protected val stateCommunicator: StateCommunicator,
+ protected val effectCommunicator: EffectCommunicator,
+ coroutineManager: CoroutineManager,
+) : ScreenModel, Reducer, Actor, ContractProvider {
+
+ private val scope get() = coroutineScope
+
+ protected var isInitialize = AtomicBoolean(false)
+
+ private val store = launchedStore(
+ scope = scope,
+ effectCommunicator = effectCommunicator,
+ stateCommunicator = stateCommunicator,
+ actor = this,
+ reducer = this,
+ coroutineManager = coroutineManager,
+ )
+
+ override fun init() {
+ isInitialize.set(true)
+ }
+
+ override fun dispatchEvent(event: E) = store.sendEvent(event)
+
+ override suspend fun collectState(collector: FlowCollector) {
+ store.collectState(collector)
+ }
+
+ override suspend fun collectUiEffect(collector: FlowCollector) {
+ store.collectUiEffect(collector)
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/BaseViewModel.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/BaseViewModel.kt
new file mode 100644
index 0000000..68ba5f8
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/BaseViewModel.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.platform.screenmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.FlowCollector
+import ru.aleshin.core.common.managers.CoroutineManager
+import ru.aleshin.core.common.platform.communications.state.EffectCommunicator
+import ru.aleshin.core.common.platform.communications.state.StateCommunicator
+import ru.aleshin.core.common.platform.screenmodel.contract.*
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+import ru.aleshin.core.common.platform.screenmodel.store.launchedStore
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.inject.Provider
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+abstract class BaseViewModel(
+ protected val stateCommunicator: StateCommunicator,
+ protected val effectCommunicator: EffectCommunicator,
+ coroutineManager: CoroutineManager,
+) : ViewModel(), Reducer, Actor, ContractProvider {
+
+ private val scope get() = viewModelScope
+
+ protected val isInitialize = AtomicBoolean(false)
+
+ private val store = launchedStore(
+ scope = scope,
+ effectCommunicator = effectCommunicator,
+ stateCommunicator = stateCommunicator,
+ actor = this,
+ reducer = this,
+ coroutineManager = coroutineManager,
+ )
+
+ override fun init() {
+ isInitialize.set(true)
+ }
+
+ override fun dispatchEvent(event: E) = store.sendEvent(event)
+
+ override suspend fun collectState(collector: FlowCollector) {
+ store.collectState(collector)
+ }
+
+ override suspend fun collectUiEffect(collector: FlowCollector) {
+ store.collectUiEffect(collector)
+ }
+
+ abstract class Factory(
+ private val viewModel: Provider,
+ ) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return viewModel.get() as T
+ }
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/ContractProviders.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/ContractProviders.kt
new file mode 100644
index 0000000..b5acc33
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/ContractProviders.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.platform.screenmodel
+
+import kotlinx.coroutines.flow.FlowCollector
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface StateProvider {
+ suspend fun collectState(collector: FlowCollector)
+}
+
+interface UiEffectProvider {
+ suspend fun collectUiEffect(collector: FlowCollector)
+}
+
+interface EventReceiver {
+ fun dispatchEvent(event: E)
+}
+
+interface ContractProvider :
+ StateProvider,
+ EventReceiver,
+ UiEffectProvider,
+ Init
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/Init.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/Init.kt
new file mode 100644
index 0000000..5a0900c
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/Init.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.platform.screenmodel
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface Init {
+ fun init()
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/Reducer.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/Reducer.kt
new file mode 100644
index 0000000..1e8f5cd
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/Reducer.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel
+
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface Reducer {
+ suspend fun reduce(action: A, currentState: S): S
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/ReetrantLock.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/ReetrantLock.kt
new file mode 100644
index 0000000..2c0df57
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/ReetrantLock.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel
+
+import kotlinx.coroutines.GlobalScope.coroutineContext
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
+
+internal suspend fun Mutex.withReentrantLock(block: suspend () -> T): T {
+ val key = ReentrantMutexContextKey(this)
+ if (coroutineContext[key] != null) return block()
+
+ return withContext(ReentrantMutexContextElement(key)) {
+ withLock { block() }
+ }
+}
+
+internal class ReentrantMutexContextElement(
+ override val key: ReentrantMutexContextKey,
+) : CoroutineContext.Element
+
+internal data class ReentrantMutexContextKey(
+ val mutex: Mutex,
+) : CoroutineContext.Key
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseAction.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseAction.kt
new file mode 100644
index 0000000..d95c42b
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseAction.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel.contract
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface BaseAction
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseEffect.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseEffect.kt
new file mode 100644
index 0000000..d9e88a9
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseEffect.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel.contract
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface BaseUiEffect
+
+interface EmptyUiEffect : BaseUiEffect
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseEvent.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseEvent.kt
new file mode 100644
index 0000000..35e9fff
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseEvent.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel.contract
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface BaseEvent
+
+
+
+
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseViewState.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseViewState.kt
new file mode 100644
index 0000000..7dc0b79
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/contract/BaseViewState.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel.contract
+
+import android.os.Parcelable
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface BaseViewState : Parcelable
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/store/BaseStore.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/store/BaseStore.kt
new file mode 100644
index 0000000..95bb8bb
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/store/BaseStore.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel.store
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.sync.Mutex
+import ru.aleshin.core.common.managers.CoroutineManager
+import ru.aleshin.core.common.platform.communications.state.StateCommunicator
+import ru.aleshin.core.common.platform.screenmodel.Actor
+import ru.aleshin.core.common.platform.screenmodel.Reducer
+import ru.aleshin.core.common.platform.screenmodel.StateProvider
+import ru.aleshin.core.common.platform.screenmodel.UiEffectProvider
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+import ru.aleshin.core.common.platform.screenmodel.withReentrantLock
+import ru.aleshin.core.common.platform.screenmodel.work.WorkScope
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface BaseStore :
+ StateProvider,
+ UiEffectProvider {
+
+ fun sendEvent(event: E)
+ suspend fun postEffect(effect: F)
+ suspend fun handleAction(action: A)
+ suspend fun updateState(transform: suspend (S) -> S)
+ suspend fun fetchState(): S
+
+ abstract class Abstract(
+ private val stateCommunicator: StateCommunicator,
+ private val actor: Actor,
+ private val reducer: Reducer,
+ private val coroutineManager: CoroutineManager,
+ ) : BaseStore {
+
+ private val mutex = Mutex()
+
+ private val eventChannel = Channel(Channel.UNLIMITED, BufferOverflow.SUSPEND)
+
+ fun start(scope: CoroutineScope) = coroutineManager.runOnBackground(scope) {
+ val workScope = WorkScope.Base(this@Abstract, scope)
+ while (isActive) {
+ actor.apply { workScope.handleEvent(eventChannel.receive()) }
+ }
+ }
+
+ override fun sendEvent(event: E) {
+ eventChannel.trySend(event)
+ }
+
+ override suspend fun fetchState(): S {
+ return stateCommunicator.read()
+ }
+
+ override suspend fun updateState(transform: suspend (S) -> S) = mutex.withReentrantLock {
+ val state = transform(stateCommunicator.read())
+ stateCommunicator.update(state)
+ }
+
+ override suspend fun handleAction(action: A) = updateState {
+ reducer.reduce(action, fetchState())
+ }
+
+ override suspend fun collectState(collector: FlowCollector) {
+ stateCommunicator.collect(collector)
+ }
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/store/SharedStore.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/store/SharedStore.kt
new file mode 100644
index 0000000..3200a30
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/store/SharedStore.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel.store
+
+import kotlinx.coroutines.flow.FlowCollector
+import ru.aleshin.core.common.managers.CoroutineManager
+import ru.aleshin.core.common.platform.communications.state.EffectCommunicator
+import ru.aleshin.core.common.platform.communications.state.StateCommunicator
+import ru.aleshin.core.common.platform.screenmodel.Actor
+import ru.aleshin.core.common.platform.screenmodel.Reducer
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+class SharedStore(
+ private val effectCommunicator: EffectCommunicator,
+ stateCommunicator: StateCommunicator,
+ actor: Actor,
+ reducer: Reducer,
+ coroutineManager: CoroutineManager,
+) : BaseStore.Abstract(stateCommunicator, actor, reducer, coroutineManager) {
+
+ override suspend fun postEffect(effect: F) {
+ effectCommunicator.update(effect)
+ }
+
+ override suspend fun collectUiEffect(collector: FlowCollector) {
+ effectCommunicator.collect(collector)
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/store/Store.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/store/Store.kt
new file mode 100644
index 0000000..57e76ab
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/store/Store.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel.store
+
+import kotlinx.coroutines.CoroutineScope
+import ru.aleshin.core.common.managers.CoroutineManager
+import ru.aleshin.core.common.platform.communications.state.EffectCommunicator
+import ru.aleshin.core.common.platform.communications.state.StateCommunicator
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+import ru.aleshin.core.common.platform.screenmodel.Actor
+import ru.aleshin.core.common.platform.screenmodel.Reducer
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+fun MVIStore(
+ effectCommunicator: EffectCommunicator,
+ stateCommunicator: StateCommunicator,
+ actor: Actor,
+ reducer: Reducer,
+ coroutineManager: CoroutineManager,
+) = SharedStore(effectCommunicator, stateCommunicator, actor, reducer, coroutineManager)
+
+fun launchedStore(
+ scope: CoroutineScope,
+ effectCommunicator: EffectCommunicator,
+ stateCommunicator: StateCommunicator,
+ actor: Actor,
+ reducer: Reducer,
+ coroutineManager: CoroutineManager,
+) = MVIStore(effectCommunicator, stateCommunicator, actor, reducer, coroutineManager).apply {
+ start(scope)
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/FlowWorkProcessor.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/FlowWorkProcessor.kt
new file mode 100644
index 0000000..36e3f6f
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/FlowWorkProcessor.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel.work
+
+import kotlinx.coroutines.flow.Flow
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface FlowWorkProcessor {
+ suspend fun work(command: C): FlowWorkResult
+}
+
+typealias FlowWorkResult = Flow>
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/WorkProcessor.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/WorkProcessor.kt
new file mode 100644
index 0000000..4ae8505
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/WorkProcessor.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel.work
+
+import ru.aleshin.core.common.functional.Either
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface WorkProcessor {
+ suspend fun work(command: C): WorkResult
+}
+
+interface WorkCommand : BackgroundWorkKey
+
+typealias WorkResult = Either
+
+typealias ActionResult = Either.Left
+typealias EffectResult = Either.Right
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/WorkResultHandler.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/WorkResultHandler.kt
new file mode 100644
index 0000000..7c25b5d
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/WorkResultHandler.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.platform.screenmodel.work
+
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface WorkResultHandler {
+ suspend fun WorkResult.handleWork()
+ suspend fun FlowWorkResult.collectAndHandleWork()
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/WorkScope.kt b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/WorkScope.kt
new file mode 100644
index 0000000..7ab7a56
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/platform/screenmodel/work/WorkScope.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.platform.screenmodel.work
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.launch
+import ru.aleshin.core.common.functional.Either
+import ru.aleshin.core.common.managers.CoroutineBlock
+import ru.aleshin.core.common.managers.CoroutineManager
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseAction
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseEvent
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseUiEffect
+import ru.aleshin.core.common.platform.screenmodel.contract.BaseViewState
+import ru.aleshin.core.common.platform.screenmodel.store.BaseStore
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface WorkScope : WorkResultHandler {
+
+ fun launchBackgroundWork(
+ key: BackgroundWorkKey,
+ scope: CoroutineScope? = null,
+ block: CoroutineBlock,
+ ): Job
+
+ suspend fun state(): S
+ suspend fun sendAction(action: A)
+ suspend fun sendEffect(effect: F)
+
+ class Base(
+ private val store: BaseStore,
+ private val coroutineScope: CoroutineScope,
+ ) : WorkScope {
+
+ private val backgroundWorkMap = mutableMapOf()
+
+ override suspend fun state(): S {
+ return store.fetchState()
+ }
+
+ override suspend fun sendAction(action: A) {
+ store.handleAction(action)
+ }
+
+ override suspend fun sendEffect(effect: F) {
+ store.postEffect(effect)
+ }
+
+ override fun launchBackgroundWork(
+ key: BackgroundWorkKey,
+ scope: CoroutineScope?,
+ block: CoroutineBlock,
+ ): Job {
+ backgroundWorkMap[key].let { job ->
+ if (job != null) {
+ job.cancel()
+ backgroundWorkMap.remove(key)
+ }
+ }
+ return (scope ?: coroutineScope).launch { block() }.apply {
+ backgroundWorkMap[key] = this
+ start()
+ }
+ }
+
+ override suspend fun Either.handleWork() = when (this) {
+ is Either.Left -> sendAction(data)
+ is Either.Right -> sendEffect(data)
+ }
+
+ override suspend fun Flow>.collectAndHandleWork() {
+ collect { it.handleWork() }
+ }
+ }
+}
+
+interface BackgroundWorkKey
diff --git a/core/common/src/main/java/ru/aleshin/core/common/services/ServiceBinder.kt b/core/common/src/main/java/ru/aleshin/core/common/services/ServiceBinder.kt
new file mode 100644
index 0000000..54ee913
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/services/ServiceBinder.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common.services
+
+import android.app.Service
+import android.os.Binder
+import java.lang.ref.WeakReference
+
+abstract class ServiceBinder : Binder() {
+
+ abstract fun fetchService(): S
+
+ abstract class Abstract(service: T) : ServiceBinder() {
+
+ private val serviceReference = WeakReference(service)
+
+ override fun fetchService(): T = checkNotNull(serviceReference.get())
+ }
+}
\ No newline at end of file
diff --git a/core/common/src/main/java/ru/aleshin/core/common/validation/ValidateError.kt b/core/common/src/main/java/ru/aleshin/core/common/validation/ValidateError.kt
new file mode 100644
index 0000000..ee2127c
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/validation/ValidateError.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.validation
+
+import android.os.Parcelable
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface ValidateError : Parcelable
diff --git a/core/common/src/main/java/ru/aleshin/core/common/validation/ValidateResult.kt b/core/common/src/main/java/ru/aleshin/core/common/validation/ValidateResult.kt
new file mode 100644
index 0000000..705ecbc
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/validation/ValidateResult.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.validation
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+data class ValidateResult(
+ val isValid: Boolean,
+ val validError: E?,
+)
+
+suspend fun ValidateResult.handle(
+ onValid: suspend () -> Unit,
+ onError: suspend (E) -> Unit,
+) = when (this.isValid) {
+ true -> onValid()
+ false -> onError(checkNotNull(this.validError))
+}
+
+suspend fun operateValidate(
+ isSuccess: suspend () -> Unit,
+ isError: suspend () -> Unit,
+ vararg isValid: Boolean,
+) {
+ when (isValid.contains(false)) {
+ true -> isError()
+ false -> isSuccess()
+ }
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/validation/Validator.kt b/core/common/src/main/java/ru/aleshin/core/common/validation/Validator.kt
new file mode 100644
index 0000000..705bf08
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/validation/Validator.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.common.validation
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface Validator {
+ fun validate(data: D): ValidateResult
+}
diff --git a/core/common/src/main/java/ru/aleshin/core/common/wrappers/EitherWrapper.kt b/core/common/src/main/java/ru/aleshin/core/common/wrappers/EitherWrapper.kt
new file mode 100644
index 0000000..a910c23
--- /dev/null
+++ b/core/common/src/main/java/ru/aleshin/core/common/wrappers/EitherWrapper.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.common.wrappers
+
+import kotlinx.coroutines.flow.*
+import ru.aleshin.core.common.functional.DomainFailures
+import ru.aleshin.core.common.functional.DomainResult
+import ru.aleshin.core.common.functional.Either
+import ru.aleshin.core.common.handlers.ErrorHandler
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface EitherWrapper {
+
+ suspend fun wrap(block: suspend () -> O): DomainResult
+
+ abstract class Abstract(
+ private val errorHandler: ErrorHandler,
+ ) : EitherWrapper {
+
+ override suspend fun wrap(block: suspend () -> O) = try {
+ Either.Right(data = block.invoke())
+ } catch (error: Throwable) {
+ Either.Left(data = errorHandler.handle(error))
+ }
+ }
+}
+
+interface FlowEitherWrapper : EitherWrapper {
+
+ suspend fun wrapFlow(block: suspend () -> Flow): Flow>
+
+ abstract class Abstract(
+ private val errorHandler: ErrorHandler,
+ ) : FlowEitherWrapper, EitherWrapper.Abstract(errorHandler) {
+
+ override suspend fun wrapFlow(block: suspend () -> Flow) = flow {
+ block.invoke().catch { error ->
+ this@flow.emit(Either.Left(data = errorHandler.handle(error)))
+ }.collect { data ->
+ emit(Either.Right(data = data))
+ }
+ }
+ }
+}
diff --git a/core/common/src/test/java/ru/aleshin/core/common/ExampleUnitTest.kt b/core/common/src/test/java/ru/aleshin/core/common/ExampleUnitTest.kt
new file mode 100644
index 0000000..009e43b
--- /dev/null
+++ b/core/common/src/test/java/ru/aleshin/core/common/ExampleUnitTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.common
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/core/ui/.gitignore b/core/ui/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/core/ui/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
new file mode 100644
index 0000000..454256e
--- /dev/null
+++ b/core/ui/build.gradle.kts
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-parcelize")
+ kotlin("kapt")
+}
+
+repositories {
+ mavenCentral()
+ google()
+}
+
+android {
+ namespace = "ru.aleshin.core.ui"
+ compileSdk = Config.compileSdkVersion
+
+ defaultConfig {
+ minSdk = Config.minSdkVersion
+
+ testInstrumentationRunner = Config.testInstrumentRunner
+ }
+
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro",
+ )
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = Config.jvmTarget
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Config.kotlinCompiler
+ }
+
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+ implementation(project(":core:common"))
+ implementation(Dependencies.AndroidX.core)
+ implementation(Dependencies.AndroidX.appcompat)
+ implementation(Dependencies.AndroidX.material)
+ implementation(Dependencies.AndroidX.placeHolder)
+
+ implementation(Dependencies.AndroidX.systemUiController)
+
+ implementation(Dependencies.Voyager.navigator)
+
+ implementation(Dependencies.Compose.ui)
+ implementation(Dependencies.Compose.activity)
+
+ implementation(Dependencies.Dagger.core)
+ kapt(Dependencies.Dagger.kapt)
+
+ testImplementation(Dependencies.Test.jUnit)
+ androidTestImplementation(Dependencies.Test.jUnitExt)
+ androidTestImplementation(Dependencies.Test.espresso)
+ androidTestImplementation(Dependencies.Test.composeJUnit)
+ debugImplementation(Dependencies.Compose.uiTooling)
+ debugImplementation(Dependencies.Compose.uiTestManifest)
+}
diff --git a/core/ui/consumer-rules.pro b/core/ui/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/core/ui/proguard-rules.pro b/core/ui/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/core/ui/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/core/ui/src/androidTest/java/ru/aleshin/core/ui/ExampleInstrumentedTest.kt b/core/ui/src/androidTest/java/ru/aleshin/core/ui/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..3631091
--- /dev/null
+++ b/core/ui/src/androidTest/java/ru/aleshin/core/ui/ExampleInstrumentedTest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.ui
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("ru.aleshin.core.ui.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/AndroidManifest.xml b/core/ui/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c193aa3
--- /dev/null
+++ b/core/ui/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/theme/MixPlayerRes.kt b/core/ui/src/main/java/ru/aleshin/core/ui/theme/MixPlayerRes.kt
new file mode 100644
index 0000000..4d4a39e
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/theme/MixPlayerRes.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.theme
+
+import androidx.compose.runtime.Composable
+import ru.aleshin.core.ui.theme.tokens.*
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+object MixPlayerRes {
+
+ val elevations: MixPlayerElevations
+ @Composable get() = LocalMixPlayerElevations.current
+
+ val language: MixPlayerLanguage
+ @Composable get() = LocalMixPlayerLanguage.current
+
+ val colorsType: MixPlayerColorsType
+ @Composable get() = LocalMixPlayerColorsType.current
+
+ val strings: MixPlayerStrings
+ @Composable get() = LocalMixPlayerStrings.current
+
+ val icons: MixPlayerIcons
+ @Composable get() = LocalMixPlayerIcons.current
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/theme/MixPlayerTheme.kt b/core/ui/src/main/java/ru/aleshin/core/ui/theme/MixPlayerTheme.kt
new file mode 100644
index 0000000..e39e3a1
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/theme/MixPlayerTheme.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.theme
+
+import androidx.compose.material3.*
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.intl.Locale
+import com.google.accompanist.systemuicontroller.rememberSystemUiController
+import ru.aleshin.core.ui.theme.material.*
+import ru.aleshin.core.ui.theme.tokens.*
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun MixPlayerTheme(
+ dynamicColor: Boolean = false,
+ themeType: ThemeUiType = ThemeUiType.DEFAULT,
+ language: LanguageUiType = LanguageUiType.DEFAULT,
+ content: @Composable () -> Unit,
+) {
+ val colorsType = MixPlayerColorsType(themeType.isDarkTheme())
+ val appLanguage = when (language) {
+ LanguageUiType.DEFAULT -> fetchAppLanguage(Locale.current.language)
+ LanguageUiType.EN -> MixPlayerLanguage.EN
+ LanguageUiType.RU -> MixPlayerLanguage.RU
+ }
+ val appStrings = fetchCoreStrings(appLanguage)
+ val appElevations = baseMixPlayerElevations
+ val appIcons = baseMixPlayerIcons
+
+ MaterialTheme(
+ colorScheme = themeType.toColorScheme(dynamicColor),
+ shapes = baseShapes,
+ typography = baseTypography,
+ ) {
+ CompositionLocalProvider(
+ LocalMixPlayerLanguage provides appLanguage,
+ LocalMixPlayerColorsType provides colorsType,
+ LocalMixPlayerElevations provides appElevations,
+ LocalMixPlayerStrings provides appStrings,
+ LocalMixPlayerIcons provides appIcons,
+ content = content,
+ )
+ MixPlayerSystemUi(
+ navigationBarColor = colorScheme.background,
+ statusBarColor = colorScheme.background,
+ isDarkIcons = themeType.isDarkTheme(),
+ )
+ }
+}
+
+@Composable
+fun MixPlayerSystemUi(navigationBarColor: Color, statusBarColor: Color, isDarkIcons: Boolean) {
+ val systemUiController = rememberSystemUiController()
+
+ SideEffect {
+ systemUiController.setNavigationBarColor(
+ color = navigationBarColor,
+ darkIcons = !isDarkIcons,
+ )
+ systemUiController.setStatusBarColor(color = statusBarColor, darkIcons = !isDarkIcons)
+ }
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/ColorSchemes.kt b/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/ColorSchemes.kt
new file mode 100644
index 0000000..b6f14e0
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/ColorSchemes.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.ui.theme.material
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+
+private val baseLightColorScheme = lightColorScheme(
+ primary = md_theme_light_primary,
+ onPrimary = md_theme_light_onPrimary,
+ primaryContainer = md_theme_light_primaryContainer,
+ onPrimaryContainer = md_theme_light_onPrimaryContainer,
+ secondary = md_theme_light_secondary,
+ onSecondary = md_theme_light_onSecondary,
+ secondaryContainer = md_theme_light_secondaryContainer,
+ onSecondaryContainer = md_theme_light_onSecondaryContainer,
+ tertiary = md_theme_light_tertiary,
+ onTertiary = md_theme_light_onTertiary,
+ tertiaryContainer = md_theme_light_tertiaryContainer,
+ onTertiaryContainer = md_theme_light_onTertiaryContainer,
+ error = md_theme_light_error,
+ onError = md_theme_light_onError,
+ errorContainer = md_theme_light_errorContainer,
+ onErrorContainer = md_theme_light_onErrorContainer,
+ outline = md_theme_light_outline,
+ background = md_theme_light_background,
+ onBackground = md_theme_light_onBackground,
+ surface = md_theme_light_surface,
+ surfaceContainerHighest = md_theme_light_surface_container_highest,
+ surfaceContainerHigh = md_theme_light_surface_container_high,
+ surfaceContainer = md_theme_light_surface_container,
+ surfaceContainerLow = md_theme_light_surface_container_low,
+ surfaceContainerLowest = md_theme_light_surface_container_lowest,
+ onSurface = md_theme_light_onSurface,
+ surfaceVariant = md_theme_light_surfaceVariant,
+ onSurfaceVariant = md_theme_light_onSurfaceVariant,
+ inverseSurface = md_theme_light_inverseSurface,
+ inverseOnSurface = md_theme_light_inverseOnSurface,
+ inversePrimary = md_theme_light_inversePrimary,
+ surfaceTint = md_theme_light_surfaceTint,
+ outlineVariant = md_theme_light_outlineVariant,
+ scrim = md_theme_light_scrim,
+)
+
+private val baseDarkColorScheme = darkColorScheme(
+ primary = md_theme_dark_primary,
+ onPrimary = md_theme_dark_onPrimary,
+ primaryContainer = md_theme_dark_primaryContainer,
+ onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+ secondary = md_theme_dark_secondary,
+ onSecondary = md_theme_dark_onSecondary,
+ secondaryContainer = md_theme_dark_secondaryContainer,
+ onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+ tertiary = md_theme_dark_tertiary,
+ onTertiary = md_theme_dark_onTertiary,
+ tertiaryContainer = md_theme_dark_tertiaryContainer,
+ onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+ error = md_theme_dark_error,
+ onError = md_theme_dark_onError,
+ errorContainer = md_theme_dark_errorContainer,
+ onErrorContainer = md_theme_dark_onErrorContainer,
+ outline = md_theme_dark_outline,
+ background = md_theme_dark_background,
+ onBackground = md_theme_dark_onBackground,
+ surface = md_theme_dark_surface,
+ surfaceContainerHighest = md_theme_dark_surface_container_highest,
+ surfaceContainerHigh = md_theme_dark_surface_container_high,
+ surfaceContainer = md_theme_dark_surface_container,
+ surfaceContainerLow = md_theme_dark_surface_container_low,
+ surfaceContainerLowest = md_theme_dark_surface_container_lowest,
+ onSurface = md_theme_dark_onSurface,
+ surfaceVariant = md_theme_dark_surfaceVariant,
+ onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+ inverseSurface = md_theme_dark_inverseSurface,
+ inverseOnSurface = md_theme_dark_inverseOnSurface,
+ inversePrimary = md_theme_dark_inversePrimary,
+ surfaceTint = md_theme_dark_surfaceTint,
+ outlineVariant = md_theme_dark_outlineVariant,
+ scrim = md_theme_dark_scrim,
+)
+
+enum class ThemeUiType {
+ DEFAULT, LIGHT, DARK
+}
+
+@Composable
+fun ThemeUiType.toColorScheme(
+ dynamicColor: Boolean = false,
+) = if (dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val context = LocalContext.current
+ when (this) {
+ ThemeUiType.DEFAULT -> if (isSystemInDarkTheme()) {
+ dynamicDarkColorScheme(context)
+ } else {
+ dynamicLightColorScheme(context)
+ }
+
+ ThemeUiType.LIGHT -> dynamicLightColorScheme(context)
+ ThemeUiType.DARK -> dynamicDarkColorScheme(context)
+ }
+} else {
+ when (this) {
+ ThemeUiType.DEFAULT -> if (isSystemInDarkTheme()) baseDarkColorScheme else baseLightColorScheme
+ ThemeUiType.LIGHT -> baseLightColorScheme
+ ThemeUiType.DARK -> baseDarkColorScheme
+ }
+}
+
+@Composable
+fun ThemeUiType.isDarkTheme() = when (this) {
+ ThemeUiType.DEFAULT -> isSystemInDarkTheme()
+ ThemeUiType.LIGHT -> false
+ ThemeUiType.DARK -> true
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/Colors.kt b/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/Colors.kt
new file mode 100644
index 0000000..7ac0732
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/Colors.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.ui.theme.material
+
+import androidx.compose.ui.graphics.Color
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+
+val md_theme_light_primary = Color(0xFF2055CF)
+val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+val md_theme_light_primaryContainer = Color(0xFFDBE1FF)
+val md_theme_light_onPrimaryContainer = Color(0xFF00174C)
+val md_theme_light_secondary = Color(0xFF595E72)
+val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+val md_theme_light_secondaryContainer = Color(0xFFDDE1F9)
+val md_theme_light_onSecondaryContainer = Color(0xFF161B2C)
+val md_theme_light_tertiary = Color(0xFF745470)
+val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+val md_theme_light_tertiaryContainer = Color(0xFFFFD6F7)
+val md_theme_light_onTertiaryContainer = Color(0xFF2B122A)
+val md_theme_light_error = Color(0xFFBA1A1A)
+val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+val md_theme_light_onError = Color(0xFFFFFFFF)
+val md_theme_light_onErrorContainer = Color(0xFF410002)
+val md_theme_light_background = Color(0xFFFEFBFF)
+val md_theme_light_onBackground = Color(0xFF1B1B1F)
+val md_theme_light_outline = Color(0xFF757680)
+val md_theme_light_inverseOnSurface = Color(0xFFF2F0F4)
+val md_theme_light_inverseSurface = Color(0xFF303034)
+val md_theme_light_inversePrimary = Color(0xFFB4C5FF)
+val md_theme_light_surfaceTint = Color(0xFF2055CF)
+val md_theme_light_outlineVariant = Color(0xFFC6C6D0)
+val md_theme_light_scrim = Color(0xFF000000)
+val md_theme_light_surface = Color(0xFFFBF8FD)
+val md_theme_light_surface_container_highest = Color(0xFFE1E3E4)
+val md_theme_light_surface_container_high = Color(0xFFE7E8EA)
+val md_theme_light_surface_container = Color(0xFFECEEF0)
+val md_theme_light_surface_container_low = Color(0xFFF2F4F5)
+val md_theme_light_surface_container_lowest = Color(0xFFFFFFFF)
+val md_theme_light_onSurface = Color(0xFF17171B)
+val md_theme_light_surfaceVariant = Color(0xFFE2E1EC)
+val md_theme_light_onSurfaceVariant = Color(0xFF4E5064)
+
+val md_theme_dark_primary = Color(0xFFB4C5FF)
+val md_theme_dark_onPrimary = Color(0xFF002979)
+val md_theme_dark_primaryContainer = Color(0xFF003DAA)
+val md_theme_dark_onPrimaryContainer = Color(0xFFDBE1FF)
+val md_theme_dark_secondary = Color(0xFFC1C5DD)
+val md_theme_dark_onSecondary = Color(0xFF2B3042)
+val md_theme_dark_secondaryContainer = Color(0xFF414659)
+val md_theme_dark_onSecondaryContainer = Color(0xFFDDE1F9)
+val md_theme_dark_tertiary = Color(0xFFE2BBDB)
+val md_theme_dark_onTertiary = Color(0xFF422740)
+val md_theme_dark_tertiaryContainer = Color(0xFF5B3D58)
+val md_theme_dark_onTertiaryContainer = Color(0xFFFFD6F7)
+val md_theme_dark_error = Color(0xFFFFB4AB)
+val md_theme_dark_errorContainer = Color(0xFF93000A)
+val md_theme_dark_onError = Color(0xFF690005)
+val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+val md_theme_dark_background = Color(0xFF1B1B1F)
+val md_theme_dark_onBackground = Color(0xFFE4E2E6)
+val md_theme_dark_outline = Color(0xFF8F909A)
+val md_theme_dark_inverseOnSurface = Color(0xFF1B1B1F)
+val md_theme_dark_inverseSurface = Color(0xFFE4E2E6)
+val md_theme_dark_inversePrimary = Color(0xFF2055CF)
+val md_theme_dark_surfaceTint = Color(0xFFB4C5FF)
+val md_theme_dark_outlineVariant = Color(0xFF45464F)
+val md_theme_dark_scrim = Color(0xFF000000)
+val md_theme_dark_surface = Color(0xFF131316)
+val md_theme_dark_surface_container_highest = Color(0xFF323537)
+val md_theme_dark_surface_container_high = Color(0xFF272A2C)
+val md_theme_dark_surface_container = Color(0xFF1D2021)
+val md_theme_dark_surface_container_low = Color(0xFF191C1D)
+val md_theme_dark_surface_container_lowest = Color(0xFF0C0F10)
+val md_theme_dark_onSurface = Color(0xFFC7C6CA)
+val md_theme_dark_surfaceVariant = Color(0xFF45464F)
+val md_theme_dark_onSurfaceVariant = Color(0xFFC6C6D0)
+
+val seed = Color(0xFF3161DB)
+val badge = Color(0xFFDB1C11)
+
+val splash = Color(0xFF3161DB)
+val onSplash = Color(0xFFFFFFFF)
+
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/Shapes.kt b/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/Shapes.kt
new file mode 100644
index 0000000..7c4cdc9
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/Shapes.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.theme.material
+
+import androidx.compose.material3.Shapes
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+val baseShapes = Shapes()
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/Typography.kt b/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/Typography.kt
new file mode 100644
index 0000000..e1644fe
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/theme/material/Typography.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.theme.material
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+
+val baseTypography = Typography(
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Bold,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp,
+ ),
+ titleMedium = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.sp,
+ ),
+ labelLarge = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.sp,
+ ),
+)
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerColorsType.kt b/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerColorsType.kt
new file mode 100644
index 0000000..75b7826
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerColorsType.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.ui.theme.tokens
+
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/**
+ * @author Stanislav Aleshin on 04.07.2023.
+ */
+data class MixPlayerColorsType(val isDark: Boolean)
+
+val LocalMixPlayerColorsType = staticCompositionLocalOf {
+ error("Colors type is not provided")
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerElevations.kt b/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerElevations.kt
new file mode 100644
index 0000000..eb58542
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerElevations.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.theme.tokens
+
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+data class MixPlayerElevations(
+ val levelZero: Dp,
+ val levelOne: Dp,
+ val levelTwo: Dp,
+ val levelThree: Dp,
+ val levelFour: Dp,
+ val levelFive: Dp,
+)
+
+val baseMixPlayerElevations = MixPlayerElevations(
+ levelZero = 0.dp,
+ levelOne = 1.dp,
+ levelTwo = 3.dp,
+ levelThree = 6.dp,
+ levelFour = 8.dp,
+ levelFive = 12.dp,
+)
+
+val LocalMixPlayerElevations = staticCompositionLocalOf {
+ error("Elevations is not provided")
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerIcons.kt b/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerIcons.kt
new file mode 100644
index 0000000..ce0d994
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerIcons.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.ui.theme.tokens
+
+import androidx.compose.runtime.staticCompositionLocalOf
+import ru.aleshin.core.ui.R
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+data class MixPlayerIcons(
+ val launcher: Int,
+ val arrowUp: Int,
+ val arrowDown: Int,
+ val time: Int,
+ val videos: Int,
+)
+
+internal val baseMixPlayerIcons = MixPlayerIcons(
+ launcher = R.drawable.ic_launcer,
+ arrowUp = R.drawable.ic_arrow_up,
+ arrowDown = R.drawable.ic_arrow_down,
+ time = R.drawable.ic_time,
+ videos = R.drawable.ic_video,
+)
+
+val LocalMixPlayerIcons = staticCompositionLocalOf {
+ error("Core Icons is not provided")
+}
+
+fun fetchCoreIcons() = baseMixPlayerIcons
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerLanguage.kt b/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerLanguage.kt
new file mode 100644
index 0000000..2183d95
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerLanguage.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.theme.tokens
+
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+enum class MixPlayerLanguage(val code: String) {
+ EN("en"), RU("ru")
+}
+
+enum class LanguageUiType {
+ DEFAULT, EN, RU
+}
+
+val LocalMixPlayerLanguage = staticCompositionLocalOf {
+ error("Language is not provided")
+}
+
+fun fetchAppLanguage(language: String) = when (language) {
+ "ru" -> MixPlayerLanguage.RU
+ "en" -> MixPlayerLanguage.EN
+ else -> MixPlayerLanguage.EN
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerStrings.kt b/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerStrings.kt
new file mode 100644
index 0000000..d7ad40a
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/theme/tokens/MixPlayerStrings.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.ui.theme.tokens
+
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+data class MixPlayerStrings(
+ val appName: String,
+ val alertDialogDismissTitle: String,
+ val alertDialogSelectConfirmTitle: String,
+ val alertDialogOkConfirmTitle: String,
+ val warningDialogTitle: String,
+ val warningDeleteConfirmTitle: String,
+ val minutesSymbol: String,
+ val hoursSymbol: String,
+ val separator: String,
+ val amFormatTitle: String,
+ val pmFormatTitle: String,
+ val purchasesListTitle: String,
+ val appNameSplash: String,
+ val dataSourcePlayerError: String,
+ val audioFocusPlayerError: String,
+ val otherPlayerError: String,
+)
+
+internal val russianMixPlayerString = MixPlayerStrings(
+ appName = "MixPlayer",
+ alertDialogDismissTitle = "Отменить",
+ alertDialogSelectConfirmTitle = "Выбрать",
+ alertDialogOkConfirmTitle = "ОК",
+ warningDialogTitle = "Предупреждение!",
+ warningDeleteConfirmTitle = "Удалить",
+ minutesSymbol = "м",
+ hoursSymbol = "ч",
+ separator = ":",
+ amFormatTitle = "AM",
+ pmFormatTitle = "PM",
+ purchasesListTitle = "Список",
+ appNameSplash = "MIX PLAYER",
+ dataSourcePlayerError = "Медиа файл повреждён!",
+ audioFocusPlayerError = "Аудио фокус потерян.",
+ otherPlayerError = "Ошибка воспроизведения аудио! Попробуйте снова."
+)
+
+internal val englishMixPlayerString = MixPlayerStrings(
+ appName = "MixPlayer",
+ alertDialogDismissTitle = "Cancel",
+ alertDialogSelectConfirmTitle = "Select",
+ alertDialogOkConfirmTitle = "OK",
+ warningDialogTitle = "Warning!",
+ warningDeleteConfirmTitle = "Delete",
+ minutesSymbol = "m",
+ hoursSymbol = "h",
+ separator = ":",
+ amFormatTitle = "AM",
+ pmFormatTitle = "PM",
+ purchasesListTitle = "List",
+ appNameSplash = "MIX PLAYER",
+ dataSourcePlayerError = "The media file is corrupted!",
+ audioFocusPlayerError = "Audio focus is lost.",
+ otherPlayerError = "Audio playback error! Try again.",
+)
+
+val LocalMixPlayerStrings = staticCompositionLocalOf {
+ error("Core Strings is not provided")
+}
+
+fun fetchCoreStrings(language: MixPlayerLanguage) = when (language) {
+ MixPlayerLanguage.EN -> englishMixPlayerString
+ MixPlayerLanguage.RU -> russianMixPlayerString
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/BasePlaceholder.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/BasePlaceholder.kt
new file mode 100644
index 0000000..f786af3
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/BasePlaceholder.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.ui.views
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
+import com.google.accompanist.placeholder.PlaceholderHighlight
+import com.google.accompanist.placeholder.placeholder
+import com.google.accompanist.placeholder.shimmer
+
+/**
+ * @author Stanislav Aleshin on 10.07.2023.
+ */
+@Composable
+fun Modifier.highPlaceholder(
+ visible: Boolean = true,
+ shape: Shape = MaterialTheme.shapes.medium,
+) = placeholder(
+ visible = visible,
+ color = MaterialTheme.colorScheme.surfaceContainerHigh,
+ shape = shape,
+ highlight = PlaceholderHighlight.shimmer(
+ highlightColor = MaterialTheme.colorScheme.surfaceVariant,
+ ),
+)
+
+@Composable
+fun Modifier.highestPlaceholder(
+ visible: Boolean = true,
+ shape: Shape = MaterialTheme.shapes.medium,
+) = placeholder(
+ visible = visible,
+ color = MaterialTheme.colorScheme.surfaceContainerHighest,
+ shape = shape,
+ highlight = PlaceholderHighlight.shimmer(
+ highlightColor = MaterialTheme.colorScheme.surfaceContainer,
+ ),
+)
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/CategoryMonogram.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/CategoryMonogram.kt
new file mode 100644
index 0000000..d974e42
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/CategoryMonogram.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.ui.views
+
+import android.graphics.drawable.VectorDrawable
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.intl.Locale
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.toUpperCase
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import ru.aleshin.core.ui.theme.material.badge
+
+@Composable
+fun IconMonogram(
+ modifier: Modifier = Modifier,
+ icon: Painter,
+ iconDescription: String?,
+ iconColor: Color,
+ iconSize: Dp = 24.dp,
+ badgeEnabled: Boolean = false,
+ backgroundColor: Color,
+) = Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center,
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(RoundedCornerShape(100.dp))
+ .background(backgroundColor),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.size(iconSize),
+ painter = icon,
+ contentDescription = iconDescription,
+ tint = iconColor,
+ )
+ }
+ if (badgeEnabled) {
+ Box(
+ modifier = Modifier
+ .padding(vertical = 4.dp, horizontal = 2.dp)
+ .align(Alignment.TopEnd)
+ .size(7.dp)
+ .clip(RoundedCornerShape(100.dp))
+ .background(badge),
+ )
+ }
+}
+
+@Composable
+fun IconMonogram(
+ modifier: Modifier = Modifier,
+ vectorIcon: ImageVector,
+ iconDescription: String?,
+ iconColor: Color,
+ iconSize: Dp = 24.dp,
+ badgeEnabled: Boolean = false,
+ backgroundColor: Color,
+) = Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center,
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(RoundedCornerShape(100.dp))
+ .background(backgroundColor),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.size(iconSize),
+ imageVector = vectorIcon,
+ contentDescription = iconDescription,
+ tint = iconColor,
+ )
+ }
+ if (badgeEnabled) {
+ Box(
+ modifier = Modifier
+ .padding(vertical = 4.dp, horizontal = 2.dp)
+ .align(Alignment.TopEnd)
+ .size(7.dp)
+ .clip(RoundedCornerShape(100.dp))
+ .background(badge),
+ )
+ }
+}
+
+
+@Composable
+fun TextMonogram(
+ modifier: Modifier = Modifier,
+ isLoading: Boolean = false,
+ text: String,
+ textColor: Color,
+ backgroundColor: Color,
+ badgeEnabled: Boolean = false,
+) = Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center,
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(RoundedCornerShape(100.dp))
+ .highestPlaceholder(visible = isLoading)
+ .background(backgroundColor),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = text.toUpperCase(Locale.current),
+ modifier = Modifier.align(Alignment.Center),
+ color = textColor,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleMedium,
+ )
+ }
+ if (badgeEnabled) {
+ Box(
+ modifier = Modifier
+ .padding(vertical = 4.dp, horizontal = 2.dp)
+ .align(Alignment.TopEnd)
+ .size(7.dp)
+ .clip(RoundedCornerShape(100.dp))
+ .background(badge),
+ )
+ }
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/DeleteWarningDialog.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/DeleteWarningDialog.kt
new file mode 100644
index 0000000..ae6957d
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/DeleteWarningDialog.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.views
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import ru.aleshin.core.ui.theme.MixPlayerRes
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun WarningDeleteDialog(
+ modifier: Modifier = Modifier,
+ text: String,
+ onDismiss: () -> Unit,
+ onAction: () -> Unit,
+) {
+ AlertDialog(
+ modifier = modifier.width(328.dp),
+ onDismissRequest = onDismiss,
+ ) {
+ Surface(
+ shape = MaterialTheme.shapes.extraLarge,
+ color = MaterialTheme.colorScheme.surface,
+ tonalElevation = MixPlayerRes.elevations.levelThree,
+ ) {
+ Column {
+ WarningDeleteDialogHeader(modifier = Modifier.fillMaxWidth())
+ Text(
+ modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp).fillMaxWidth(),
+ text = text,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ DialogButtons(
+ confirmTitle = MixPlayerRes.strings.warningDeleteConfirmTitle,
+ onConfirmClick = onAction,
+ onCancelClick = onDismiss,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+internal fun WarningDeleteDialogHeader(
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.padding(start = 24.dp, end = 24.dp, top = 24.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Icon(
+ imageVector = Icons.Default.Delete,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.secondary,
+ )
+ Text(
+ text = MixPlayerRes.strings.warningDialogTitle,
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.headlineSmall,
+ )
+ }
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/DialogButtons.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/DialogButtons.kt
new file mode 100644
index 0000000..5391e1a
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/DialogButtons.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.ui.views
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import ru.aleshin.core.ui.theme.MixPlayerRes
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun DialogButtons(
+ modifier: Modifier = Modifier,
+ isConfirmEnabled: Boolean = true,
+ confirmTitle: String = MixPlayerRes.strings.alertDialogSelectConfirmTitle,
+ onCancelClick: () -> Unit,
+ onConfirmClick: () -> Unit,
+) {
+ Row(
+ modifier = modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Spacer(modifier = Modifier.weight(1f))
+ TextButton(onClick = onCancelClick) {
+ Text(
+ text = MixPlayerRes.strings.alertDialogDismissTitle,
+ color = MaterialTheme.colorScheme.primary,
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ TextButton(enabled = isConfirmEnabled, onClick = onConfirmClick) {
+ Text(
+ text = confirmTitle,
+ color = when (isConfirmEnabled) {
+ true -> MaterialTheme.colorScheme.primary
+ false -> MaterialTheme.colorScheme.onSurfaceVariant
+ },
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ }
+}
+
+@Composable
+fun DialogButtons(
+ modifier: Modifier = Modifier,
+ confirmFirstTitle: String,
+ confirmSecondTitle: String,
+ onCancelClick: () -> Unit,
+ onConfirmFirstClick: () -> Unit,
+ onConfirmSecondClick: () -> Unit,
+) {
+ Row(
+ modifier = modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp, start = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ TextButton(onClick = onCancelClick) {
+ Text(
+ text = MixPlayerRes.strings.alertDialogDismissTitle,
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ TextButton(onClick = onConfirmFirstClick) {
+ Text(
+ text = confirmFirstTitle,
+ color = MaterialTheme.colorScheme.primary,
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ TextButton(onClick = onConfirmSecondClick) {
+ Text(
+ text = confirmSecondTitle,
+ color = MaterialTheme.colorScheme.primary,
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ }
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/ErrorSnackbar.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/ErrorSnackbar.kt
new file mode 100644
index 0000000..3cbd48b
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/ErrorSnackbar.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.views
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Snackbar
+import androidx.compose.material3.SnackbarData
+import androidx.compose.runtime.Composable
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun ErrorSnackbar(snackbarData: SnackbarData) = Snackbar(
+ snackbarData = snackbarData,
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer,
+ actionColor = MaterialTheme.colorScheme.error,
+ dismissActionContentColor = MaterialTheme.colorScheme.onSurface,
+)
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/ExpandedIcon.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/ExpandedIcon.kt
new file mode 100644
index 0000000..49b468a
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/ExpandedIcon.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.views
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import ru.aleshin.core.ui.theme.MixPlayerRes
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun ExpandedIcon(
+ modifier: Modifier = Modifier,
+ isExpanded: Boolean,
+ color: Color = MaterialTheme.colorScheme.onSurface,
+ description: String? = null,
+) {
+ val icon = when (isExpanded) {
+ true -> painterResource(MixPlayerRes.icons.arrowUp)
+ false -> painterResource(MixPlayerRes.icons.arrowDown)
+ }
+ Box(modifier = modifier.animateContentSize()) {
+ Icon(
+ painter = icon,
+ contentDescription = description,
+ tint = color,
+ )
+ }
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/MinutesAndHoursTitle.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/MinutesAndHoursTitle.kt
new file mode 100644
index 0000000..2fecdd3
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/MinutesAndHoursTitle.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.views
+
+import androidx.compose.runtime.Composable
+import ru.aleshin.core.common.extensions.toMinutesAndHoursString
+import ru.aleshin.core.common.extensions.toMinutesOrHoursString
+import ru.aleshin.core.ui.theme.MixPlayerRes
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun Long.toMinutesOrHoursTitle(): String {
+ val minutesSymbols = MixPlayerRes.strings.minutesSymbol
+ val hoursSymbols = MixPlayerRes.strings.hoursSymbol
+
+ return this.toMinutesOrHoursString(minutesSymbols, hoursSymbols)
+}
+
+@Composable
+fun Long.toMinutesAndHoursTitle(): String {
+ val minutesSymbols = MixPlayerRes.strings.minutesSymbol
+ val hoursSymbols = MixPlayerRes.strings.hoursSymbol
+
+ return this.toMinutesAndHoursString(minutesSymbols, hoursSymbols)
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/NavigationDrawer.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/NavigationDrawer.kt
new file mode 100644
index 0000000..a030927
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/NavigationDrawer.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.ui.views
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import ru.aleshin.core.common.managers.DrawerItem
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun - DrawerItems(
+ modifier: Modifier = Modifier,
+ selectedItemIndex: Int,
+ items: Array
- ,
+ isAlwaysSelected: Boolean = false,
+ onItemSelected: (Item) -> Unit,
+) {
+ items.forEachIndexed { index, item ->
+ DrawerItem(
+ modifier = modifier.height(54.dp).padding(end = 12.dp),
+ selected = index == selectedItemIndex,
+ onClick = {
+ if (isAlwaysSelected || selectedItemIndex != index) onItemSelected.invoke(item)
+ },
+ icon = {
+ Icon(painter = painterResource(item.icon), contentDescription = null)
+ },
+ label = {
+ Text(
+ text = item.title,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ )
+ },
+ colors = NavigationDrawerItemDefaults.colors(
+ selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
+ selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ ),
+ )
+ }
+}
+
+@Composable
+fun DrawerTitle(
+ modifier: Modifier = Modifier,
+ title: String,
+) {
+ Box(modifier = modifier.padding(start = 16.dp, end = 8.dp, top = 8.dp, bottom = 8.dp)) {
+ Text(
+ text = title,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.titleSmall,
+ )
+ }
+}
+
+@Composable
+fun DrawerSectionHeader(
+ modifier: Modifier = Modifier,
+ header: String,
+) {
+ Box(modifier = modifier.padding(vertical = 18.dp, horizontal = 16.dp)) {
+ Text(
+ text = header,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.titleSmall,
+ )
+ }
+}
+
+@Composable
+fun DrawerItem(
+ modifier: Modifier = Modifier,
+ selected: Boolean,
+ onClick: () -> Unit,
+ icon: (@Composable () -> Unit)? = null,
+ label: @Composable () -> Unit,
+ badge: (@Composable () -> Unit)? = null,
+ shape: Shape = RoundedCornerShape(topEnd = 100.dp, bottomEnd = 100.dp),
+ colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+) {
+ Surface(
+ modifier = modifier.height(50.dp).fillMaxWidth(),
+ selected = selected,
+ onClick = onClick,
+ shape = shape,
+ color = colors.containerColor(selected).value,
+ interactionSource = interactionSource,
+ ) {
+ Row(
+ Modifier.padding(start = 16.dp, end = 24.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (icon != null) {
+ val iconColor = colors.iconColor(selected).value
+ CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
+ Spacer(Modifier.width(20.dp))
+ }
+ Box(Modifier.weight(1f)) {
+ val labelColor = colors.textColor(selected).value
+ CompositionLocalProvider(LocalContentColor provides labelColor, content = label)
+ }
+ if (badge != null) {
+ Spacer(Modifier.width(12.dp))
+ val badgeColor = colors.badgeColor(selected).value
+ CompositionLocalProvider(LocalContentColor provides badgeColor, content = badge)
+ }
+ }
+ }
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/SegmentedButtons.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/SegmentedButtons.kt
new file mode 100644
index 0000000..29bb0d1
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/SegmentedButtons.kt
@@ -0,0 +1,155 @@
+package ru.aleshin.core.ui.views
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface SegmentedButtonItem {
+ val title: String
+ @Composable
+ @ReadOnlyComposable
+ get
+}
+
+@Composable
+fun
- SegmentedButtons(
+ modifier: Modifier = Modifier,
+ items: Array
- ,
+ selectedItem: Item,
+ onItemClick: (Item) -> Unit,
+) {
+ Row(modifier = modifier.fillMaxWidth()) {
+ items.forEachIndexed { index, item ->
+ if (index == 0) {
+ SegmentedButton(
+ modifier = Modifier.weight(1f),
+ title = item.title,
+ isSelected = item == selectedItem,
+ shape = SegmentedButtonDefaults.firstButtonShape(),
+ onClick = { onItemClick.invoke(item) },
+ )
+ } else if (items.lastIndex == index) {
+ SegmentedButton(
+ modifier = Modifier.weight(1f),
+ title = item.title,
+ isSelected = item == selectedItem,
+ shape = SegmentedButtonDefaults.lastButtonShape(),
+ onClick = { onItemClick.invoke(item) },
+ )
+ } else {
+ SegmentedButton(
+ modifier = Modifier.weight(1f),
+ title = item.title,
+ isSelected = item == selectedItem,
+ shape = SegmentedButtonDefaults.centerButtonShape(),
+ onClick = { onItemClick.invoke(item) },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun SegmentedButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ title: String,
+ shape: Shape,
+ isSelected: Boolean,
+) {
+ OutlinedButton(
+ onClick = onClick,
+ modifier = modifier.height(SegmentedButtonDefaults.height),
+ contentPadding = SegmentedButtonDefaults.contentPadding(),
+ colors = SegmentedButtonDefaults.buttonColors(isSelected = isSelected),
+ shape = shape,
+ ) {
+ if (isSelected) {
+ Icon(
+ modifier = Modifier
+ .size(SegmentedButtonDefaults.selectedIconSize)
+ .padding(end = SegmentedButtonDefaults.selectedIconPadding),
+ imageVector = Icons.Default.Check,
+ contentDescription = "Selected",
+ )
+ }
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
+ textAlign = TextAlign.Center,
+ )
+ }
+}
+
+@Composable
+fun SegmentedButtonCornerShape(
+ cornerStart: Dp = 0.dp,
+ cornerEnd: Dp = 0.dp,
+) = RoundedCornerShape(
+ topStart = cornerStart,
+ bottomStart = cornerStart,
+ topEnd = cornerEnd,
+ bottomEnd = cornerEnd,
+)
+
+object SegmentedButtonDefaults {
+
+ val height = 40.dp
+
+ val selectedIconSize = 16.dp
+
+ val selectedIconPadding = 4.dp
+
+ private val horizontalContentPadding = 4.dp
+
+ private val verticalContentPadding = 0.dp
+
+ private val shapeCorner = 100.dp
+
+ @Composable
+ fun contentPadding(
+ horizontal: Dp = horizontalContentPadding,
+ vertical: Dp = verticalContentPadding,
+ ) = PaddingValues(horizontal = horizontal, vertical = vertical)
+
+ @Composable
+ fun selectedButtonColors(): ButtonColors = ButtonDefaults.filledTonalButtonColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ )
+
+ @Composable
+ fun defaultButtonColors(): ButtonColors = ButtonDefaults.outlinedButtonColors()
+
+ @Composable
+ fun buttonColors(isSelected: Boolean): ButtonColors = if (isSelected) {
+ selectedButtonColors()
+ } else {
+ defaultButtonColors()
+ }
+
+ @Composable
+ fun firstButtonShape(corner: Dp = shapeCorner): RoundedCornerShape =
+ SegmentedButtonCornerShape(cornerStart = corner)
+
+ @Composable
+ fun centerButtonShape(corner: Dp = 0.dp): RoundedCornerShape =
+ SegmentedButtonCornerShape(cornerStart = corner, cornerEnd = corner)
+
+ @Composable
+ fun lastButtonShape(corner: Dp = shapeCorner): RoundedCornerShape =
+ SegmentedButtonCornerShape(cornerEnd = corner)
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/TabNavigationBar.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/TabNavigationBar.kt
new file mode 100644
index 0000000..ad28bfa
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/TabNavigationBar.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.views
+
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.res.painterResource
+import ru.aleshin.core.ui.theme.MixPlayerRes
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun
- BottomNavigationBar(
+ modifier: Modifier,
+ selectedItem: Item?,
+ items: Array
- ,
+ showLabel: Boolean,
+ onItemSelected: (Item) -> Unit,
+) {
+ NavigationBar(
+ modifier = modifier,
+ containerColor = MaterialTheme.colorScheme.background,
+ tonalElevation = MixPlayerRes.elevations.levelZero,
+ ) {
+ items.forEach { item ->
+ NavigationBarItem(
+ selected = selectedItem == item,
+ onClick = { onItemSelected.invoke(item) },
+ icon = {
+ BottomBarIcon(
+ selected = selectedItem == item,
+ enabledIcon = painterResource(id = item.enabledIcon),
+ disabledIcon = painterResource(id = item.disabledIcon),
+ description = item.label,
+ )
+ },
+ label = if (showLabel) { {
+ BottomBarLabel(
+ selected = selectedItem == item,
+ title = item.label,
+ )
+ } } else {
+ null
+ },
+ colors = NavigationBarItemDefaults.colors(
+ indicatorColor = MaterialTheme.colorScheme.primaryContainer,
+ ),
+ )
+ }
+ }
+}
+
+@Composable
+fun BottomBarIcon(
+ selected: Boolean,
+ enabledIcon: Painter,
+ disabledIcon: Painter,
+ description: String,
+) {
+ Icon(
+ painter = if (selected) enabledIcon else disabledIcon,
+ contentDescription = description,
+ tint = when (selected) {
+ true -> MaterialTheme.colorScheme.onSecondaryContainer
+ false -> MaterialTheme.colorScheme.onSurfaceVariant
+ },
+ )
+}
+
+@Composable
+fun BottomBarLabel(
+ selected: Boolean,
+ title: String,
+) {
+ Text(
+ text = title,
+ color = when (selected) {
+ true -> MaterialTheme.colorScheme.onSurface
+ false -> MaterialTheme.colorScheme.onSurfaceVariant
+ },
+ style = MaterialTheme.typography.labelMedium,
+ )
+}
+
+interface BottomBarItem {
+ val label: String @Composable get
+ val enabledIcon: Int @Composable get
+ val disabledIcon: Int @Composable get
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/TimeFormatSelector.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/TimeFormatSelector.kt
new file mode 100644
index 0000000..69cbf1c
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/TimeFormatSelector.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.ui.views
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import ru.aleshin.core.common.functional.TimeFormat
+import ru.aleshin.core.ui.theme.MixPlayerRes
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun TimeFormatSelector(
+ modifier: Modifier = Modifier,
+ isVisible: Boolean,
+ format: TimeFormat,
+ onChangeFormat: (TimeFormat) -> Unit,
+) {
+ if (isVisible) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(0.dp),
+ ) {
+ TextButton(
+ modifier = Modifier.weight(1f),
+ onClick = { onChangeFormat(TimeFormat.AM) },
+ shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
+ colors = ButtonDefaults.textButtonColors(
+ containerColor = when (format) {
+ TimeFormat.AM -> MaterialTheme.colorScheme.primaryContainer
+ TimeFormat.PM -> MaterialTheme.colorScheme.surfaceContainerHigh
+ },
+ ),
+ ) {
+ Text(
+ text = MixPlayerRes.strings.amFormatTitle,
+ color = when (format) {
+ TimeFormat.AM -> MaterialTheme.colorScheme.onPrimaryContainer
+ TimeFormat.PM -> MaterialTheme.colorScheme.onSurfaceVariant
+ },
+ style = MaterialTheme.typography.labelMedium,
+ )
+ }
+ TextButton(
+ modifier = Modifier.weight(1f),
+ onClick = { onChangeFormat(TimeFormat.PM) },
+ shape = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp),
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
+ colors = ButtonDefaults.textButtonColors(
+ containerColor = when (format) {
+ TimeFormat.PM -> MaterialTheme.colorScheme.primaryContainer
+ TimeFormat.AM -> MaterialTheme.colorScheme.surfaceContainerHigh
+ },
+ ),
+ ) {
+ Text(
+ text = MixPlayerRes.strings.pmFormatTitle,
+ color = when (format) {
+ TimeFormat.AM -> MaterialTheme.colorScheme.onSurfaceVariant
+ TimeFormat.PM -> MaterialTheme.colorScheme.onPrimaryContainer
+ },
+ style = MaterialTheme.typography.labelMedium,
+ )
+ }
+ }
+ }
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/TimePickerDialog.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/TimePickerDialog.kt
new file mode 100644
index 0000000..b457eb1
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/TimePickerDialog.kt
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.ui.views
+
+import android.text.format.DateFormat
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import ru.aleshin.core.common.functional.TimeFormat
+import ru.aleshin.core.ui.theme.MixPlayerRes
+import java.util.*
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun TimePickerDialog(
+ modifier: Modifier = Modifier,
+ headerTitle: String,
+ initTime: Date,
+ onDismissRequest: () -> Unit,
+ onSelectedTime: (Date) -> Unit,
+) {
+ val calendar = Calendar.getInstance().apply { time = initTime }
+ val is24Format = DateFormat.is24HourFormat(LocalContext.current)
+ var hours by rememberSaveable { mutableStateOf(calendar.get(Calendar.HOUR_OF_DAY)) }
+ var minutes by rememberSaveable { mutableStateOf(calendar.get(Calendar.MINUTE)) }
+ var format by remember {
+ mutableStateOf(if (hours != null && hours!! > 11) TimeFormat.PM else TimeFormat.AM)
+ }
+
+ AlertDialog(onDismissRequest = onDismissRequest) {
+ Surface(
+ modifier = modifier.width(if (is24Format) 243.dp else 348.dp),
+ tonalElevation = MixPlayerRes.elevations.levelThree,
+ shape = MaterialTheme.shapes.extraLarge,
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(20.dp),
+ horizontalAlignment = Alignment.End,
+ ) {
+ TimePickerHeader(title = headerTitle)
+ TimePickerHourMinuteSelector(
+ hours = hours,
+ minutes = minutes,
+ format = format,
+ is24Format = is24Format,
+ onHoursChanges = { value -> hours = value },
+ onMinutesChanges = { value -> minutes = value },
+ onChangeFormat = {
+ hours = null
+ format = it
+ },
+ )
+ TimePickerActions(
+ enabledConfirm = minutes in 0..59 && hours in 0..23,
+ onDismissClick = onDismissRequest,
+ onCurrentTimeChoose = {
+ val currentTime = Calendar.getInstance()
+ hours = currentTime.get(Calendar.HOUR_OF_DAY)
+ minutes = currentTime.get(Calendar.MINUTE) + 1
+ if (!is24Format && (hours!! > 12 || hours == 0)) format = TimeFormat.PM
+ },
+ onConfirmClick = {
+ val time = calendar.apply {
+ set(Calendar.HOUR_OF_DAY, checkNotNull(hours))
+ set(Calendar.MINUTE, checkNotNull(minutes))
+ set(Calendar.SECOND, 0)
+ set(Calendar.MILLISECOND, 0)
+ }.time
+ onSelectedTime.invoke(time)
+ },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+internal fun TimePickerHeader(
+ modifier: Modifier = Modifier,
+ title: String,
+) = Box(
+ modifier = modifier.padding(start = 24.dp, end = 24.dp, top = 24.dp).fillMaxWidth(),
+) {
+ Text(
+ text = title,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelMedium,
+ )
+}
+
+@Composable
+internal fun TimePickerHourMinuteSelector(
+ modifier: Modifier = Modifier,
+ hours: Int?,
+ minutes: Int?,
+ is24Format: Boolean,
+ format: TimeFormat,
+ onHoursChanges: (Int?) -> Unit,
+ onMinutesChanges: (Int?) -> Unit,
+ onChangeFormat: (TimeFormat) -> Unit,
+) = Row(
+ modifier = modifier.padding(horizontal = 24.dp),
+ verticalAlignment = Alignment.CenterVertically,
+) {
+ val requester = remember { FocusRequester() }
+ OutlinedTextField(
+ modifier = Modifier.weight(1f),
+ value = if (is24Format) {
+ hours?.toString() ?: ""
+ } else {
+ when {
+ hours == 0 && format == TimeFormat.AM -> "12"
+ hours == 0 && format == TimeFormat.PM -> "12"
+ format == TimeFormat.PM && hours != 12 -> hours?.minus(12)?.toString() ?: ""
+ else -> hours?.toString() ?: ""
+ }
+ },
+ onValueChange = {
+ val time = it.toIntOrNull()
+ if (time != null && is24Format && time in 0..23) {
+ onHoursChanges(time)
+ } else if (time != null && !is24Format && time in 1..12) {
+ val formatTime = when (format) {
+ TimeFormat.PM -> if (time != 12) time + 12 else 12
+ TimeFormat.AM -> if (time != 12) time else 0
+ }
+ onHoursChanges(formatTime)
+ } else if (it.isBlank()) {
+ onHoursChanges(null)
+ }
+ },
+ textStyle = MaterialTheme.typography.displayMedium.copy(textAlign = TextAlign.Center),
+ shape = MaterialTheme.shapes.small,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedContainerColor = MaterialTheme.colorScheme.primaryContainer,
+ focusedBorderColor = MaterialTheme.colorScheme.primary,
+ unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
+ unfocusedBorderColor = MaterialTheme.colorScheme.surfaceVariant,
+ ),
+ )
+ Text(
+ modifier = Modifier.width(24.dp),
+ text = MixPlayerRes.strings.separator,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.displayLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ OutlinedTextField(
+ modifier = Modifier.weight(1f).focusRequester(requester),
+ value = minutes?.toString() ?: "",
+ onValueChange = {
+ val time = it.toIntOrNull()
+ if (time != null && time in 0..59) {
+ onMinutesChanges(time)
+ } else if (it.isBlank()) {
+ onMinutesChanges(null)
+ }
+ },
+ textStyle = MaterialTheme.typography.displayMedium.copy(textAlign = TextAlign.Center),
+ shape = MaterialTheme.shapes.small,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ singleLine = true,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedContainerColor = MaterialTheme.colorScheme.primaryContainer,
+ focusedBorderColor = MaterialTheme.colorScheme.primary,
+ unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
+ unfocusedBorderColor = MaterialTheme.colorScheme.surfaceVariant,
+ ),
+ )
+ TimeFormatSelector(
+ modifier = Modifier
+ .size(height = 80.dp, width = 52.dp)
+ .offset(x = 12.dp),
+ isVisible = !is24Format,
+ format = format,
+ onChangeFormat = onChangeFormat,
+ )
+}
+
+@Composable
+internal fun TimePickerActions(
+ modifier: Modifier = Modifier,
+ enabledConfirm: Boolean = true,
+ onDismissClick: () -> Unit,
+ onCurrentTimeChoose: () -> Unit,
+ onConfirmClick: () -> Unit,
+) = Row(
+ modifier = modifier.padding(bottom = 20.dp, start = 16.dp, end = 24.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+) {
+ IconButton(onClick = onCurrentTimeChoose) {
+ Icon(
+ painter = painterResource(MixPlayerRes.icons.time),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ TextButton(onClick = onDismissClick) {
+ Text(text = MixPlayerRes.strings.alertDialogDismissTitle)
+ }
+ TextButton(enabled = enabledConfirm, onClick = onConfirmClick) {
+ Text(text = MixPlayerRes.strings.alertDialogSelectConfirmTitle)
+ }
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/TopAppBar.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/TopAppBar.kt
new file mode 100644
index 0000000..29942a3
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/TopAppBar.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+package ru.aleshin.core.ui.views
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+@Composable
+fun TopAppBarTitle(
+ modifier: Modifier = Modifier,
+ textAlign: TextAlign = TextAlign.Start,
+ text: String,
+ subText: String? = null,
+) {
+ Column(modifier = modifier) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = text,
+ textAlign = textAlign,
+ color = MaterialTheme.colorScheme.onBackground,
+ style = MaterialTheme.typography.titleLarge,
+ maxLines = 1,
+ )
+ if (subText != null) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = subText,
+ textAlign = textAlign,
+ color = MaterialTheme.colorScheme.onBackground,
+ style = MaterialTheme.typography.titleSmall,
+ )
+ }
+ }
+}
+
+@Composable
+fun TopAppBarEmptyButton(modifier: Modifier = Modifier) {
+ Spacer(modifier = modifier.size(48.dp))
+}
+
+@Composable
+fun TopAppBarButton(
+ modifier: Modifier = Modifier,
+ imageVector: ImageVector,
+ imageDescription: String?,
+ onButtonClick: () -> Unit,
+) {
+ IconButton(
+ modifier = modifier.size(48.dp),
+ onClick = onButtonClick,
+ ) {
+ Icon(
+ imageVector = imageVector,
+ contentDescription = imageDescription,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+}
+
+@Composable
+fun TopAppBarButton(
+ modifier: Modifier = Modifier,
+ imagePainter: Painter,
+ imageDescription: String,
+ onButtonClick: () -> Unit,
+) {
+ IconButton(
+ modifier = modifier.size(48.dp),
+ onClick = onButtonClick,
+ ) {
+ Icon(
+ painter = imagePainter,
+ contentDescription = imageDescription,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+}
+
+@Composable
+fun TopAppBarMoreActions(
+ modifier: Modifier = Modifier,
+ items: Array,
+ onItemClick: (T) -> Unit,
+ moreIconDescription: String,
+) {
+ val expanded = rememberSaveable { mutableStateOf(false) }
+ Box(modifier = modifier.wrapContentSize(Alignment.TopEnd)) {
+ IconButton(onClick = { expanded.value = true }) {
+ Icon(
+ imageVector = Icons.Filled.MoreVert,
+ contentDescription = moreIconDescription,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+
+ DropdownMenu(
+ modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainerHigh),
+ expanded = expanded.value,
+ offset = DpOffset(0.dp, 10.dp),
+ onDismissRequest = { expanded.value = false },
+ ) {
+ items.forEach { item ->
+ DropdownMenuItem(
+ text = {
+ Text(
+ modifier = Modifier.defaultMinSize(minWidth = 150.dp),
+ text = item.title,
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.titleMedium,
+ )
+ },
+ leadingIcon = if (item.icon != null) { {
+ Icon(
+ painter = painterResource(checkNotNull(item.icon)),
+ contentDescription = item.title,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ } } else {
+ null
+ },
+ onClick = {
+ expanded.value = false
+ onItemClick.invoke(item)
+ },
+ )
+ }
+ }
+ }
+}
diff --git a/core/ui/src/main/java/ru/aleshin/core/ui/views/TopAppBarAction.kt b/core/ui/src/main/java/ru/aleshin/core/ui/views/TopAppBarAction.kt
new file mode 100644
index 0000000..3d19f32
--- /dev/null
+++ b/core/ui/src/main/java/ru/aleshin/core/ui/views/TopAppBarAction.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package ru.aleshin.core.ui.views
+
+import androidx.compose.runtime.Composable
+
+/**
+ * @author Stanislav Aleshin on 12.06.2023.
+ */
+interface TopAppBarAction {
+ val icon: Int? @Composable get
+ val title: String @Composable get
+ val isAlwaysShow: Boolean
+}
diff --git a/core/ui/src/main/res/drawable/ic_arrow_down.xml b/core/ui/src/main/res/drawable/ic_arrow_down.xml
new file mode 100644
index 0000000..c1c897a
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_arrow_down.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_arrow_up.xml b/core/ui/src/main/res/drawable/ic_arrow_up.xml
new file mode 100644
index 0000000..10c5932
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_arrow_up.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_launcer.xml b/core/ui/src/main/res/drawable/ic_launcer.xml
new file mode 100644
index 0000000..87850c0
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_launcer.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_time.xml b/core/ui/src/main/res/drawable/ic_time.xml
new file mode 100644
index 0000000..bdf91f9
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_time.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_video.xml b/core/ui/src/main/res/drawable/ic_video.xml
new file mode 100644
index 0000000..154eca5
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_video.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/core/ui/src/test/java/ru/aleshin/core/ui/ExampleUnitTest.kt b/core/ui/src/test/java/ru/aleshin/core/ui/ExampleUnitTest.kt
new file mode 100644
index 0000000..491fe8f
--- /dev/null
+++ b/core/ui/src/test/java/ru/aleshin/core/ui/ExampleUnitTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.core.ui
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/features/home/api/.gitignore b/features/home/api/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/features/home/api/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/features/home/api/build.gradle.kts b/features/home/api/build.gradle.kts
new file mode 100644
index 0000000..8111ae9
--- /dev/null
+++ b/features/home/api/build.gradle.kts
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-parcelize")
+ kotlin("kapt")
+}
+
+repositories {
+ mavenCentral()
+ google()
+}
+
+android {
+ namespace = "ru.aleshin.features.home.api"
+ compileSdk = Config.compileSdkVersion
+
+ defaultConfig {
+ minSdk = Config.minSdkVersion
+
+ testInstrumentationRunner = Config.testInstrumentRunner
+ consumerProguardFiles(Config.consumerProguardFiles)
+ }
+
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro",
+ )
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = Config.jvmTarget
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Config.kotlinCompiler
+ }
+
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+
+ implementation(project(":module_injector"))
+ implementation(project(":core:common"))
+ implementation(project(":core:ui"))
+
+ implementation(Dependencies.Voyager.navigator)
+
+ implementation(Dependencies.AndroidX.core)
+ implementation(Dependencies.AndroidX.appcompat)
+ implementation(Dependencies.AndroidX.material)
+
+ implementation(Dependencies.Compose.ui)
+ implementation(Dependencies.Compose.activity)
+
+ implementation(Dependencies.Dagger.core)
+
+ testImplementation(Dependencies.Test.jUnit)
+ androidTestImplementation(Dependencies.Test.jUnitExt)
+ androidTestImplementation(Dependencies.Test.espresso)
+}
diff --git a/features/home/api/consumer-rules.pro b/features/home/api/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/features/home/api/proguard-rules.pro b/features/home/api/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/features/home/api/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/features/home/api/src/androidTest/java/ru/aleshin/features/foods/api/ExampleInstrumentedTest.kt b/features/home/api/src/androidTest/java/ru/aleshin/features/foods/api/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..8e2e7e9
--- /dev/null
+++ b/features/home/api/src/androidTest/java/ru/aleshin/features/foods/api/ExampleInstrumentedTest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.foods.api
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("ru.aleshin.features.foods.api.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/features/home/api/src/main/AndroidManifest.xml b/features/home/api/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c193aa3
--- /dev/null
+++ b/features/home/api/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/AppAudioLocalDataSource.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/AppAudioLocalDataSource.kt
new file mode 100644
index 0000000..b0a5b3b
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/AppAudioLocalDataSource.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.datasources
+
+import android.content.Context
+import ru.aleshin.features.home.api.data.models.AudioInfoModel
+import javax.inject.Inject
+
+
+/**
+ * @author Stanislav Aleshin on 13.07.2023.
+ */
+interface AppAudioLocalDataSource {
+
+ suspend fun fetchAllAppTracks(): List
+
+ class Base @Inject constructor(
+ private val applicationContext: Context,
+ ) : AppAudioLocalDataSource {
+
+ override suspend fun fetchAllAppTracks(): List {
+ // TODO: Add data
+ return emptyList()
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/AudioStoreManager.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/AudioStoreManager.kt
new file mode 100644
index 0000000..6414d77
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/AudioStoreManager.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.datasources
+
+import android.content.Context
+import android.provider.MediaStore
+import ru.aleshin.features.home.api.data.models.AudioInfoModel
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+interface AudioStoreManager {
+
+ fun fetchAllMedia(): List?
+
+ class Base @Inject constructor(
+ private val applicationContext: Context,
+ private val queryParser: MediaQueryParser,
+ ) : AudioStoreManager {
+
+ private val resolver get() = applicationContext.contentResolver
+
+ override fun fetchAllMedia(): List? {
+ val mediaUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+ val query = resolver.query(mediaUri, null, null, null, null)
+ return queryParser.parseAudio(query)?.sortedByDescending { it.duration }
+ }
+ }
+}
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/MediaQueryParser.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/MediaQueryParser.kt
new file mode 100644
index 0000000..8ee8e39
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/MediaQueryParser.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.datasources
+
+import android.database.Cursor
+import ru.aleshin.core.common.managers.AudioStoreUtils
+import ru.aleshin.core.common.managers.VideoStoreUtils
+import ru.aleshin.features.home.api.data.models.AudioInfoModel
+import ru.aleshin.features.home.api.data.models.VideoInfoModel
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+interface MediaQueryParser {
+
+ fun parseAudio(cursor: Cursor?): List?
+ fun parseVideo(cursor: Cursor?): List?
+
+ class Base @Inject constructor() : MediaQueryParser {
+
+ override fun parseAudio(cursor: Cursor?) = when {
+ cursor == null -> null
+ !cursor.moveToFirst() -> emptyList()
+ else -> with(AudioStoreUtils) {
+ mutableListOf().apply {
+ do {
+ val path = checkNotNull(fetchPath(cursor))
+ val info = AudioInfoModel(
+ id = checkNotNull(fetchId(cursor)),
+ path = path,
+ title = checkNotNull(fetchTitle(cursor)),
+ artist = fetchArtist(cursor),
+ album = fetchAlbum(cursor),
+ duration = checkNotNull(fetchDuration(cursor)),
+ imagePath = path,
+ date = checkNotNull(fetchDate(cursor)),
+ )
+ add(info)
+ } while (cursor.moveToNext())
+ }
+ }
+ }.apply { cursor?.close() }
+
+ override fun parseVideo(cursor: Cursor?) = when {
+ cursor == null -> null
+ !cursor.moveToFirst() -> emptyList()
+ else -> with(VideoStoreUtils) {
+ mutableListOf().apply {
+ do {
+ val path = checkNotNull(fetchPath(cursor))
+ val info = VideoInfoModel(
+ id = checkNotNull(fetchId(cursor)),
+ path = path,
+ title = checkNotNull(fetchTitle(cursor)),
+ imagePath = path,
+ duration = checkNotNull(fetchDuration(cursor)),
+ )
+ add(info)
+ } while (cursor.moveToNext())
+ }
+ }
+ }.apply { cursor?.close() }
+ }
+}
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/SystemAudioLocalDataSource.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/SystemAudioLocalDataSource.kt
new file mode 100644
index 0000000..5b650e3
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/SystemAudioLocalDataSource.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.datasources
+
+import ru.aleshin.features.home.api.data.models.AudioInfoModel
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+interface SystemAudioLocalDataSource {
+
+ suspend fun fetchAllSystemTracks(): List
+
+ class Base @Inject constructor(
+ private val audioStoreManager: AudioStoreManager,
+ ) : SystemAudioLocalDataSource {
+
+ override suspend fun fetchAllSystemTracks(): List {
+ return checkNotNull(audioStoreManager.fetchAllMedia())
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/VideosLocalDataSource.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/VideosLocalDataSource.kt
new file mode 100644
index 0000000..4679561
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/VideosLocalDataSource.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.datasources
+
+import ru.aleshin.features.home.api.data.models.VideoInfoModel
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+interface VideosLocalDataSource {
+
+ fun fetchAllVideos(): List
+
+ class Base @Inject constructor(
+ private val videoStoreManager: VideosStoreManager
+ ) : VideosLocalDataSource {
+
+ override fun fetchAllVideos(): List {
+ return checkNotNull(videoStoreManager.fetchAllMedia())
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/VideosStoreManager.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/VideosStoreManager.kt
new file mode 100644
index 0000000..aed2712
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/datasources/VideosStoreManager.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.datasources
+
+import android.content.Context
+import android.provider.MediaStore
+import android.util.Log
+import ru.aleshin.features.home.api.data.models.VideoInfoModel
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+interface VideosStoreManager {
+
+ fun fetchAllMedia(): List?
+
+ class Base @Inject constructor(
+ private val applicationContext: Context,
+ private val mediaQueryParser: MediaQueryParser,
+ ) : VideosStoreManager {
+
+ private val resolver get() = applicationContext.contentResolver
+
+ override fun fetchAllMedia(): List? {
+ val mediaUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+ val query = resolver.query(mediaUri, null, null, null, null)
+ return mediaQueryParser.parseVideo(query)?.sortedByDescending { it.duration }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/mappers/AudioDataMappers.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/mappers/AudioDataMappers.kt
new file mode 100644
index 0000000..95b87dd
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/mappers/AudioDataMappers.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.mappers
+
+import ru.aleshin.features.home.api.data.models.AudioInfoModel
+import ru.aleshin.features.home.api.domain.entities.AudioInfo
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+fun AudioInfoModel.mapToDomain() = AudioInfo(
+ id = id,
+ path = path,
+ title = title,
+ artist = artist,
+ album = album,
+ imagePath = imagePath,
+ durationInMillis = duration,
+ date = date,
+)
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/mappers/VideoDataMappers.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/mappers/VideoDataMappers.kt
new file mode 100644
index 0000000..4340ccb
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/mappers/VideoDataMappers.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.mappers
+
+import ru.aleshin.core.common.managers.BitmapUtils
+import ru.aleshin.features.home.api.data.models.VideoInfoModel
+import ru.aleshin.features.home.api.domain.entities.VideoInfo
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+fun VideoInfoModel.mapToDomain() = VideoInfo(
+ id = id,
+ path = path,
+ title = title,
+ imagePath = imagePath,
+ duration = duration,
+)
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/models/AudioInfoModel.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/models/AudioInfoModel.kt
new file mode 100644
index 0000000..b11b29d
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/models/AudioInfoModel.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.models
+
+import java.util.Date
+
+/**
+ * @author Stanislav Aleshin on 14.07.2023.
+ */
+data class AudioInfoModel(
+ val id: Long,
+ val path: String,
+ val title: String,
+ val artist: String?,
+ val album: String? = null,
+ val imagePath: String?,
+ val duration: Long,
+ val date: Date = Date(),
+)
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/models/VideoInfoModel.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/models/VideoInfoModel.kt
new file mode 100644
index 0000000..2fcff6c
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/models/VideoInfoModel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.models
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+data class VideoInfoModel(
+ val id: Long,
+ val path: String,
+ val title: String,
+ val imagePath: String?,
+ val duration: Long,
+)
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/repository/AppAudioRepositoryImpl.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/repository/AppAudioRepositoryImpl.kt
new file mode 100644
index 0000000..b349180
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/repository/AppAudioRepositoryImpl.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.repository
+
+import ru.aleshin.features.home.api.data.datasources.AppAudioLocalDataSource
+import ru.aleshin.features.home.api.data.mappers.mapToDomain
+import ru.aleshin.core.common.functional.audio.AudioPlayListType
+import ru.aleshin.features.home.api.domain.entities.AudioPlayList
+import ru.aleshin.features.home.api.domain.repositories.AppAudioRepository
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 13.07.2023.
+ */
+class AppAudioRepositoryImpl @Inject constructor(
+ private val localDataSource: AppAudioLocalDataSource,
+) : AppAudioRepository {
+
+ override suspend fun fetchPlaylist(): AudioPlayList {
+ val tracks = localDataSource.fetchAllAppTracks().map { audioInfo ->
+ audioInfo.mapToDomain()
+ }
+ return AudioPlayList(type = AudioPlayListType.APP, audioList = tracks)
+ }
+}
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/repository/SystemAudioRepositoryImpl.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/repository/SystemAudioRepositoryImpl.kt
new file mode 100644
index 0000000..4c013b0
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/repository/SystemAudioRepositoryImpl.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.repository
+
+import ru.aleshin.features.home.api.data.datasources.SystemAudioLocalDataSource
+import ru.aleshin.features.home.api.data.mappers.mapToDomain
+import ru.aleshin.core.common.functional.audio.AudioPlayListType
+import ru.aleshin.features.home.api.domain.entities.AudioPlayList
+import ru.aleshin.features.home.api.domain.repositories.SystemAudioRepository
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+class SystemAudioRepositoryImpl @Inject constructor(
+ private val localDataSource: SystemAudioLocalDataSource,
+) : SystemAudioRepository {
+
+ override suspend fun fetchPlaylist(): AudioPlayList {
+ val tracks = localDataSource.fetchAllSystemTracks().map { audioInfo ->
+ audioInfo.mapToDomain()
+ }
+ return AudioPlayList(type = AudioPlayListType.SYSTEM, audioList = tracks)
+ }
+}
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/data/repository/VideosRepositoryImpl.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/repository/VideosRepositoryImpl.kt
new file mode 100644
index 0000000..681ad02
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/data/repository/VideosRepositoryImpl.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.data.repository
+
+import ru.aleshin.features.home.api.data.datasources.VideosLocalDataSource
+import ru.aleshin.features.home.api.data.mappers.mapToDomain
+import ru.aleshin.features.home.api.domain.entities.VideoInfo
+import ru.aleshin.features.home.api.domain.repositories.VideosRepository
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+class VideosRepositoryImpl @Inject constructor(
+ private val localDataSource: VideosLocalDataSource,
+) : VideosRepository {
+
+ override suspend fun fetchVideos(): List {
+ return localDataSource.fetchAllVideos().map { it.mapToDomain() }
+ }
+}
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/di/HomeFeatureApi.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/di/HomeFeatureApi.kt
new file mode 100644
index 0000000..bd55ab4
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/di/HomeFeatureApi.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.di
+
+import ru.aleshin.features.home.api.navigation.HomeFeatureStarter
+import ru.aleshin.module_injector.BaseFeatureApi
+
+/**
+ * @author Stanislav Aleshin on 14.06.2023.
+ */
+interface HomeFeatureApi : BaseFeatureApi {
+ fun fetchStarter(): HomeFeatureStarter
+}
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/di/HomeScreens.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/di/HomeScreens.kt
new file mode 100644
index 0000000..088b39d
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/di/HomeScreens.kt
@@ -0,0 +1,12 @@
+package ru.aleshin.features.home.api.di
+
+import ru.aleshin.core.common.functional.audio.AudioPlayListUi
+
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+sealed class HomeScreens {
+ object Home : HomeScreens()
+ data class Details(val playList: AudioPlayListUi) : HomeScreens()
+}
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/entities/AudioInfo.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/entities/AudioInfo.kt
new file mode 100644
index 0000000..9dea607
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/entities/AudioInfo.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.domain.entities
+
+import java.util.Date
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+data class AudioInfo(
+ val id: Long,
+ val path: String,
+ val title: String,
+ val artist: String?,
+ val album: String?,
+ val imagePath: String?,
+ val durationInMillis: Long,
+ val date: Date,
+)
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/entities/AudioPlayList.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/entities/AudioPlayList.kt
new file mode 100644
index 0000000..0d4446f
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/entities/AudioPlayList.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.domain.entities
+
+import ru.aleshin.core.common.functional.audio.AudioPlayListType
+
+/**
+ * @author Stanislav Aleshin on 13.07.2023.
+ */
+data class AudioPlayList(
+ val type: AudioPlayListType,
+ val audioList: List,
+)
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/entities/VideoInfo.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/entities/VideoInfo.kt
new file mode 100644
index 0000000..3476f18
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/entities/VideoInfo.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.domain.entities
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+data class VideoInfo(
+ val id: Long,
+ val path: String,
+ val title: String,
+ val imagePath: String?,
+ val duration: Long,
+)
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/repositories/AppAudioRepository.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/repositories/AppAudioRepository.kt
new file mode 100644
index 0000000..fc601e8
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/repositories/AppAudioRepository.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.domain.repositories
+
+import ru.aleshin.features.home.api.domain.entities.AudioPlayList
+
+/**
+ * @author Stanislav Aleshin on 13.07.2023.
+ */
+interface AppAudioRepository {
+ suspend fun fetchPlaylist(): AudioPlayList
+}
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/repositories/SystemAudioRepository.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/repositories/SystemAudioRepository.kt
new file mode 100644
index 0000000..dcee44f
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/repositories/SystemAudioRepository.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.domain.repositories
+
+import ru.aleshin.features.home.api.domain.entities.AudioPlayList
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+interface SystemAudioRepository {
+ suspend fun fetchPlaylist(): AudioPlayList
+}
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/repositories/VideosRepository.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/repositories/VideosRepository.kt
new file mode 100644
index 0000000..3cdecd5
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/domain/repositories/VideosRepository.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.domain.repositories
+
+import ru.aleshin.features.home.api.domain.entities.VideoInfo
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+interface VideosRepository {
+ suspend fun fetchVideos(): List
+}
\ No newline at end of file
diff --git a/features/home/api/src/main/java/ru/aleshin/features/home/api/navigation/HomeFeatureStarter.kt b/features/home/api/src/main/java/ru/aleshin/features/home/api/navigation/HomeFeatureStarter.kt
new file mode 100644
index 0000000..20c6986
--- /dev/null
+++ b/features/home/api/src/main/java/ru/aleshin/features/home/api/navigation/HomeFeatureStarter.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api.navigation
+
+import cafe.adriel.voyager.core.screen.Screen
+import ru.aleshin.features.home.api.di.HomeScreens
+
+/**
+ * @author Stanislav Aleshin on 14.06.2023.
+ */
+interface HomeFeatureStarter {
+ suspend fun fetchHomeScreen(navScreen: HomeScreens): Screen
+}
diff --git a/features/home/api/src/test/java/ru/aleshin/features/home/api/ExampleUnitTest.kt b/features/home/api/src/test/java/ru/aleshin/features/home/api/ExampleUnitTest.kt
new file mode 100644
index 0000000..4a054ad
--- /dev/null
+++ b/features/home/api/src/test/java/ru/aleshin/features/home/api/ExampleUnitTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.api
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/features/home/impl/.gitignore b/features/home/impl/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/features/home/impl/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts
new file mode 100644
index 0000000..7cf3268
--- /dev/null
+++ b/features/home/impl/build.gradle.kts
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-parcelize")
+ kotlin("kapt")
+}
+
+repositories {
+ google()
+ mavenCentral()
+}
+
+android {
+ namespace = "ru.aleshin.features.home.impl"
+ compileSdk = Config.compileSdkVersion
+
+ defaultConfig {
+ minSdk = Config.minSdkVersion
+
+ testInstrumentationRunner = Config.testInstrumentRunner
+ consumerProguardFiles(Config.consumerProguardFiles)
+ }
+
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro",
+ )
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = Config.jvmTarget
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Config.kotlinCompiler
+ }
+
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+
+ implementation(project(":module_injector"))
+ implementation(project(":core:common"))
+ implementation(project(":core:ui"))
+ implementation(project(":features:home:api"))
+ implementation(project(":features:player:api"))
+ implementation(project(":features:settings:api"))
+
+ implementation(Dependencies.AndroidX.core)
+ implementation(Dependencies.AndroidX.appcompat)
+ implementation(Dependencies.AndroidX.lifecycleRuntime)
+ implementation(Dependencies.AndroidX.material)
+ implementation(Dependencies.AndroidX.placeHolder)
+ implementation(Dependencies.AndroidX.systemUiController)
+ implementation(Dependencies.AndroidX.refresh)
+
+ implementation(Dependencies.Compose.ui)
+ implementation(Dependencies.Compose.activity)
+
+ implementation(Dependencies.ExoPlayer.library)
+ implementation(Dependencies.ExoPlayer.ui)
+
+ implementation(Dependencies.Glide.library)
+
+ implementation(Dependencies.Dagger.core)
+ kapt(Dependencies.Dagger.kapt)
+
+ implementation(Dependencies.Voyager.navigator)
+ implementation(Dependencies.Voyager.screenModel)
+ implementation(Dependencies.Voyager.transitions)
+
+ testImplementation(Dependencies.Test.jUnit)
+ testImplementation(Dependencies.Test.turbine)
+ androidTestImplementation(Dependencies.Test.jUnitExt)
+ androidTestImplementation(Dependencies.Test.espresso)
+ androidTestImplementation(Dependencies.Test.composeJUnit)
+ debugImplementation(Dependencies.Compose.uiTooling)
+ debugImplementation(Dependencies.Compose.uiTestManifest)
+}
diff --git a/features/home/impl/consumer-rules.pro b/features/home/impl/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/features/home/impl/proguard-rules.pro b/features/home/impl/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/features/home/impl/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/features/home/impl/src/androidTest/java/ru/aleshin/features/foods/impl/ExampleInstrumentedTest.kt b/features/home/impl/src/androidTest/java/ru/aleshin/features/foods/impl/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..e0ed64c
--- /dev/null
+++ b/features/home/impl/src/androidTest/java/ru/aleshin/features/foods/impl/ExampleInstrumentedTest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.foods.impl
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("ru.aleshin.features.foods.impl.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/features/home/impl/src/main/AndroidManifest.xml b/features/home/impl/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c193aa3
--- /dev/null
+++ b/features/home/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/HomeFeatureDependencies.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/HomeFeatureDependencies.kt
new file mode 100644
index 0000000..408f12d
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/HomeFeatureDependencies.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.di
+
+import ru.aleshin.core.common.managers.CoroutineManager
+import ru.aleshin.core.common.navigation.Router
+import ru.aleshin.features.home.api.data.datasources.VideosLocalDataSource
+import ru.aleshin.features.home.api.domain.repositories.AppAudioRepository
+import ru.aleshin.features.home.api.domain.repositories.SystemAudioRepository
+import ru.aleshin.features.home.api.domain.repositories.VideosRepository
+import ru.aleshin.features.player.api.navigation.PlayerFeatureStarter
+import ru.aleshin.features.settings.api.navigation.SettingsFeatureStarter
+import ru.aleshin.module_injector.BaseFeatureDependencies
+
+/**
+ * @author Stanislav Aleshin on 14.06.2023.
+ */
+interface HomeFeatureDependencies : BaseFeatureDependencies {
+ val videosRepository: VideosRepository
+ val appAudioRepository: AppAudioRepository
+ val systemAudioRepository: SystemAudioRepository
+ val playerFeatureStarter: PlayerFeatureStarter
+ val settingsFeatureStarter: SettingsFeatureStarter
+ val globalRouter: Router
+ val coroutineManager: CoroutineManager
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/component/HomeComponent.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/component/HomeComponent.kt
new file mode 100644
index 0000000..36a6afd
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/component/HomeComponent.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.di.component
+
+import dagger.Component
+import ru.aleshin.core.common.di.FeatureScope
+import ru.aleshin.core.common.navigation.navigator.NavigatorManager
+import ru.aleshin.features.home.api.di.HomeFeatureApi
+import ru.aleshin.features.home.impl.di.HomeFeatureDependencies
+import ru.aleshin.features.home.impl.di.modules.DataModule
+import ru.aleshin.features.home.impl.di.modules.DomainModule
+import ru.aleshin.features.home.impl.di.modules.NavigationModule
+import ru.aleshin.features.home.impl.di.modules.PresentationModule
+import ru.aleshin.features.home.impl.presentation.details.screenmodel.DetailsScreenModel
+import ru.aleshin.features.home.impl.presentation.home.screenmodel.HomeScreenModel
+
+/**
+ * @author Stanislav Aleshin on 14.06.2023.
+ */
+@FeatureScope
+@Component(
+ modules = [DataModule::class, DomainModule::class, PresentationModule::class, NavigationModule::class],
+ dependencies = [HomeFeatureDependencies::class],
+)
+internal interface HomeComponent : HomeFeatureApi {
+
+ fun fetchLocalNavigatorManager(): NavigatorManager
+ fun fetchHomeScreenModel(): HomeScreenModel
+ fun fetchDetailsScreenModel(): DetailsScreenModel
+
+ @Component.Builder
+ interface Builder {
+ fun dependencies(deps: HomeFeatureDependencies): Builder
+ fun build(): HomeComponent
+ }
+
+ companion object {
+ fun create(deps: HomeFeatureDependencies): HomeComponent {
+ return DaggerHomeComponent.builder()
+ .dependencies(deps)
+ .build()
+ }
+ }
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/holder/HomeComponentHolder.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/holder/HomeComponentHolder.kt
new file mode 100644
index 0000000..4b37a23
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/holder/HomeComponentHolder.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.di.holder
+
+import ru.aleshin.features.home.api.di.HomeFeatureApi
+import ru.aleshin.features.home.impl.di.HomeFeatureDependencies
+import ru.aleshin.features.home.impl.di.component.HomeComponent
+import ru.aleshin.module_injector.BaseComponentHolder
+
+/**
+ * @author Stanislav Aleshin on 14.06.2023.
+ */
+object HomeComponentHolder : BaseComponentHolder {
+
+ private var component: HomeComponent? = null
+
+ override fun init(dependencies: HomeFeatureDependencies) {
+ if (component == null) component = HomeComponent.create(dependencies)
+ }
+
+ override fun clear() {
+ component = null
+ }
+
+ override fun fetchApi(): HomeFeatureApi {
+ return fetchComponent()
+ }
+
+ internal fun fetchComponent() = checkNotNull(component) {
+ "Home component is not initialized"
+ }
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/DataModule.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/DataModule.kt
new file mode 100644
index 0000000..cc68c75
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/DataModule.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.di.modules
+
+import dagger.Module
+
+/**
+ * @author Stanislav Aleshin on 13.07.2023.
+ */
+@Module
+internal interface DataModule
\ No newline at end of file
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/DomainModule.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/DomainModule.kt
new file mode 100644
index 0000000..58ba6f5
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/DomainModule.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.di.modules
+
+import dagger.Binds
+import dagger.Module
+import ru.aleshin.core.common.di.FeatureScope
+import ru.aleshin.features.home.impl.domain.common.HomeEitherWrapper
+import ru.aleshin.features.home.impl.domain.common.HomeErrorHandler
+import ru.aleshin.features.home.impl.domain.interactors.AudioInteractor
+import ru.aleshin.features.home.impl.domain.interactors.VideosInteractor
+
+/**
+ * @author Stanislav Aleshin on 11.07.2023.
+ */
+@Module
+internal interface DomainModule {
+
+ @Binds
+ @FeatureScope
+ fun bindHomeErrorHandler(handler: HomeErrorHandler.Base): HomeErrorHandler
+
+ @Binds
+ @FeatureScope
+ fun bindHomeEitherWrapper(wrapper: HomeEitherWrapper.Base): HomeEitherWrapper
+
+
+ @Binds
+ fun bindAudioInteractor(interactor: AudioInteractor.Base): AudioInteractor
+
+ @Binds
+ fun bindVideosInteractor(interactor: VideosInteractor.Base): VideosInteractor
+}
\ No newline at end of file
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/NavigationModule.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/NavigationModule.kt
new file mode 100644
index 0000000..e7e8522
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/NavigationModule.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.di.modules
+
+import dagger.Binds
+import dagger.Module
+import ru.aleshin.core.common.di.FeatureRouter
+import ru.aleshin.core.common.di.FeatureScope
+import ru.aleshin.core.common.navigation.CommandBuffer
+import ru.aleshin.core.common.navigation.NavigationProcessor
+import ru.aleshin.core.common.navigation.Router
+import ru.aleshin.core.common.navigation.navigator.NavigatorManager
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+@Module
+internal interface NavigationModule {
+
+ @Binds
+ @FeatureScope
+ fun bindLocalCommandBuffer(buffer: CommandBuffer.Base): CommandBuffer
+
+ @Binds
+ @FeatureScope
+ fun bindLocalNavigationProcessor(processor: NavigationProcessor.Base): NavigationProcessor
+
+ @Binds
+ @FeatureScope
+ fun bindLocalNavigatorManager(manager: NavigatorManager.Base): NavigatorManager
+
+ @Binds
+ @FeatureRouter
+ @FeatureScope
+ fun bindLocalRouter(router: Router.Base): Router
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/PresentationModule.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/PresentationModule.kt
new file mode 100644
index 0000000..01ccb64
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/di/modules/PresentationModule.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.di.modules
+
+import cafe.adriel.voyager.core.model.ScreenModel
+import cafe.adriel.voyager.core.screen.Screen
+import dagger.Binds
+import dagger.Module
+import ru.aleshin.core.common.di.FeatureScope
+import ru.aleshin.core.common.di.ScreenModelKey
+import ru.aleshin.features.home.api.navigation.HomeFeatureStarter
+import ru.aleshin.features.home.impl.navigation.HomeFeatureStarterImpl
+import ru.aleshin.features.home.impl.navigation.HomeNavigationManager
+import ru.aleshin.features.home.impl.presentation.details.screenmodel.DetailsEffectCommunicator
+import ru.aleshin.features.home.impl.presentation.details.screenmodel.DetailsInfoCommunicator
+import ru.aleshin.features.home.impl.presentation.details.screenmodel.DetailsScreenModel
+import ru.aleshin.features.home.impl.presentation.details.screenmodel.DetailsStateCommunicator
+import ru.aleshin.features.home.impl.presentation.details.screenmodel.DetailsWorkProcessor
+import ru.aleshin.features.home.impl.presentation.home.HomeScreen
+import ru.aleshin.features.home.impl.presentation.home.screenmodel.HomeEffectCommunicator
+import ru.aleshin.features.home.impl.presentation.home.screenmodel.HomeScreenModel
+import ru.aleshin.features.home.impl.presentation.home.screenmodel.HomeStateCommunicator
+import ru.aleshin.features.home.impl.presentation.home.screenmodel.HomeWorkProcessor
+import ru.aleshin.features.home.impl.presentation.nav.NavScreen
+
+/**
+ * @author Stanislav Aleshin on 14.06.2023.
+ */
+@Module
+internal interface PresentationModule {
+
+ @Binds
+ @FeatureScope
+ fun bindHomeFeatureStarter(starter: HomeFeatureStarterImpl): HomeFeatureStarter
+
+ @Binds
+ @FeatureScope
+ fun bindNavScreen(screen: NavScreen): Screen
+
+ @Binds
+ @FeatureScope
+ fun bindNavigationManager(manager: HomeNavigationManager.Base): HomeNavigationManager
+
+ // Home
+
+ @Binds
+ @ScreenModelKey(HomeScreenModel::class)
+ fun bindHomeScreenModel(screenModel: HomeScreenModel): ScreenModel
+
+ @Binds
+ @FeatureScope
+ fun bindHomeStateCommunicator(communicator: HomeStateCommunicator.Base): HomeStateCommunicator
+
+ @Binds
+ fun bindHomeEffectCommunicator(communicator: HomeEffectCommunicator.Base): HomeEffectCommunicator
+
+ @Binds
+ fun bindHomeWorkProcessor(processor: HomeWorkProcessor.Base): HomeWorkProcessor
+
+ // Details
+
+ @Binds
+ @ScreenModelKey(DetailsScreenModel::class)
+ fun bindDetailsScreenModel(screenModel: DetailsScreenModel): ScreenModel
+
+ @Binds
+ fun bindDetailsStateCommunicator(communicator: DetailsStateCommunicator.Base): DetailsStateCommunicator
+
+ @Binds
+ fun bindDetailsEffectCommunicator(communicator: DetailsEffectCommunicator.Base): DetailsEffectCommunicator
+
+ @Binds
+ @FeatureScope
+ fun bindDetailsInfoCommunicator(communicator: DetailsInfoCommunicator.Base): DetailsInfoCommunicator
+
+ @Binds
+ fun bindDetailsWorkProcessor(processor: DetailsWorkProcessor.Base): DetailsWorkProcessor
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/common/HomeEitherWrapper.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/common/HomeEitherWrapper.kt
new file mode 100644
index 0000000..c0ce4ff
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/common/HomeEitherWrapper.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.domain.common
+
+import ru.aleshin.core.common.wrappers.FlowEitherWrapper
+import ru.aleshin.features.home.impl.domain.entities.HomeFailures
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 17.06.2023
+ */
+internal interface HomeEitherWrapper : FlowEitherWrapper {
+
+ class Base @Inject constructor(
+ errorHandler: HomeErrorHandler,
+ ) : HomeEitherWrapper, FlowEitherWrapper.Abstract(errorHandler)
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/common/HomeErrorHandler.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/common/HomeErrorHandler.kt
new file mode 100644
index 0000000..d8af9ae
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/common/HomeErrorHandler.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.domain.common
+
+import ru.aleshin.core.common.handlers.ErrorHandler
+import ru.aleshin.features.home.impl.domain.entities.HomeFailures
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 17.06.2023
+ */
+internal interface HomeErrorHandler : ErrorHandler {
+
+ class Base @Inject constructor() : HomeErrorHandler {
+
+ override fun handle(throwable: Throwable) = when (throwable) {
+ else -> HomeFailures.OtherError(throwable)
+ }
+ }
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/entities/HomeFailures.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/entities/HomeFailures.kt
new file mode 100644
index 0000000..c89958e
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/entities/HomeFailures.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.domain.entities
+
+import ru.aleshin.core.common.functional.DomainFailures
+
+/**
+ * @author Stanislav Aleshin on 17.06.2023.
+ */
+internal sealed class HomeFailures : DomainFailures {
+ data class OtherError(val throwable: Throwable) : HomeFailures()
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/interactors/AudioInteractor.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/interactors/AudioInteractor.kt
new file mode 100644
index 0000000..acf1597
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/interactors/AudioInteractor.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.domain.interactors
+
+import ru.aleshin.core.common.functional.DomainResult
+import ru.aleshin.features.home.impl.domain.common.HomeEitherWrapper
+import ru.aleshin.features.home.impl.domain.entities.HomeFailures
+import ru.aleshin.features.home.api.domain.entities.AudioPlayList
+import ru.aleshin.features.home.api.domain.repositories.AppAudioRepository
+import ru.aleshin.features.home.api.domain.repositories.SystemAudioRepository
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+internal interface AudioInteractor {
+
+ suspend fun fetchPlaylists(): DomainResult>
+
+ class Base @Inject constructor(
+ private val systemAudioRepository: SystemAudioRepository,
+ private val appAudioRepository: AppAudioRepository,
+ private val eitherWrapper: HomeEitherWrapper,
+ ) : AudioInteractor {
+
+ override suspend fun fetchPlaylists() = eitherWrapper.wrap {
+ mutableListOf().apply {
+ add(systemAudioRepository.fetchPlaylist())
+ add(appAudioRepository.fetchPlaylist())
+ }
+ }
+ }
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/interactors/VideosInteractor.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/interactors/VideosInteractor.kt
new file mode 100644
index 0000000..f892d06
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/domain/interactors/VideosInteractor.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.domain.interactors
+
+import ru.aleshin.core.common.functional.DomainResult
+import ru.aleshin.features.home.api.domain.entities.VideoInfo
+import ru.aleshin.features.home.api.domain.repositories.VideosRepository
+import ru.aleshin.features.home.impl.domain.common.HomeEitherWrapper
+import ru.aleshin.features.home.impl.domain.entities.HomeFailures
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 15.07.2023.
+ */
+internal interface VideosInteractor {
+
+ suspend fun fetchPlaylists(): DomainResult>
+
+ class Base @Inject constructor(
+ private val videosRepository: VideosRepository,
+ private val eitherWrapper: HomeEitherWrapper,
+ ) : VideosInteractor {
+
+ override suspend fun fetchPlaylists() = eitherWrapper.wrap {
+ videosRepository.fetchVideos()
+ }
+ }
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/navigation/HomeFeatureStarterImpl.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/navigation/HomeFeatureStarterImpl.kt
new file mode 100644
index 0000000..aeb9b04
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/navigation/HomeFeatureStarterImpl.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.navigation
+
+import cafe.adriel.voyager.core.screen.Screen
+import ru.aleshin.features.home.api.di.HomeScreens
+import ru.aleshin.features.home.api.navigation.HomeFeatureStarter
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 14.06.2023.
+ */
+internal class HomeFeatureStarterImpl @Inject constructor(
+ private val navScreen: Screen,
+ private val navigationManager: HomeNavigationManager,
+) : HomeFeatureStarter {
+
+ override suspend fun fetchHomeScreen(navScreen: HomeScreens) = navigationManager.navigateToLocalScreen(
+ navScreen = navScreen,
+ isRoot = true
+ ).let {
+ return@let this.navScreen
+ }
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/navigation/HomeNavigationManager.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/navigation/HomeNavigationManager.kt
new file mode 100644
index 0000000..fdeb4a6
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/navigation/HomeNavigationManager.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.navigation
+
+import android.util.Log
+import cafe.adriel.voyager.core.screen.Screen
+import ru.aleshin.core.common.di.FeatureRouter
+import ru.aleshin.core.common.navigation.Router
+import ru.aleshin.features.home.api.di.HomeScreens
+import ru.aleshin.features.home.impl.presentation.home.HomeScreen
+import ru.aleshin.features.home.impl.presentation.details.DetailsScreen
+import ru.aleshin.features.home.impl.presentation.details.screenmodel.DetailsInfoCommunicator
+import ru.aleshin.features.home.impl.presentation.mappers.mapToDomain
+import ru.aleshin.features.player.api.navigation.PlayerFeatureStarter
+import ru.aleshin.features.player.api.navigation.PlayerScreens
+import ru.aleshin.features.settings.api.navigation.SettingsFeatureStarter
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * @author Stanislav Aleshin on 11.07.2023.
+ */
+internal interface HomeNavigationManager {
+
+ suspend fun navigateToLocalScreen(navScreen: HomeScreens, isRoot: Boolean = false)
+ fun navigateToPlayer(screen: PlayerScreens)
+ fun navigateToSettings()
+ fun navigateToLocalBack()
+
+ class Base @Inject constructor(
+ @FeatureRouter private val localRouter: Router,
+ private val globalRouter: Router,
+ private val playerFeatureStarter: Provider,
+ private val settingsFeatureStarter: Provider,
+ private val detailsInfoCommunicator: DetailsInfoCommunicator,
+ ) : HomeNavigationManager {
+
+ override suspend fun navigateToLocalScreen(navScreen: HomeScreens, isRoot: Boolean) = when (navScreen) {
+ is HomeScreens.Home -> {
+ localNav(HomeScreen(), isRoot)
+ }
+ is HomeScreens.Details -> {
+ localNav(DetailsScreen(), isRoot)
+ detailsInfoCommunicator.update(navScreen.playList)
+ }
+ }
+
+ override fun navigateToPlayer(screen: PlayerScreens) {
+ globalRouter.navigateTo(playerFeatureStarter.get().fetchPlayerScreen(screen))
+ }
+
+ override fun navigateToSettings() {
+ globalRouter.navigateTo(settingsFeatureStarter.get().fetchMainScreen())
+ }
+
+ override fun navigateToLocalBack() = localRouter.navigateBack()
+
+ private fun localNav(screen: Screen, isRoot: Boolean) = with(localRouter) {
+ if (isRoot) replaceTo(screen, true) else navigateTo(screen)
+ }
+ }
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/DetailsContent.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/DetailsContent.kt
new file mode 100644
index 0000000..82919c1
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/DetailsContent.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.presentation.details
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.unit.dp
+import ru.aleshin.core.common.extensions.mapAudioPathToPreview
+import ru.aleshin.core.common.extensions.toSecondsAndMinutesString
+import ru.aleshin.core.common.functional.audio.AudioInfoUi
+import ru.aleshin.core.common.managers.toImageBitmap
+import ru.aleshin.features.home.impl.presentation.details.contract.DetailsViewState
+import ru.aleshin.features.home.impl.presentation.home.views.EmptyAudioItem
+import ru.aleshin.features.home.impl.presentation.home.views.AudioItem
+import ru.aleshin.features.home.impl.presentation.home.views.AudioItemPlaceholder
+import java.text.SimpleDateFormat
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+@Composable
+internal fun DetailsContent(
+ modifier: Modifier = Modifier,
+ state: DetailsViewState,
+ onItemClick: (AudioInfoUi) -> Unit,
+) {
+ LazyVerticalGrid(
+ modifier = modifier,
+ columns = GridCells.Fixed(2),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ userScrollEnabled = !state.isLoading,
+ ) {
+ val playlist = state.playlist
+ if (playlist != null && playlist.audioList.isNotEmpty()) {
+ items(playlist.audioList, key = { it.id }) { track ->
+ AudioItem(
+ modifier = Modifier.fillMaxWidth(),
+ image = track.imagePath?.mapAudioPathToPreview(),
+ title = track.title,
+ authorOrAlbum = track.artist ?: track.album ?: "",
+ duration = track.duration.toSecondsAndMinutesString(),
+ onClick = { onItemClick(track) },
+ )
+ }
+ } else if (playlist != null) {
+ item { EmptyAudioItem() }
+ } else {
+ items(15) { AudioItemPlaceholder(modifier = Modifier.fillMaxWidth()) }
+ }
+ }
+}
+
+//@Composable
+//@Preview(showBackground = true)
+//private fun DetailsContent_PreviewLight() {
+// MixPlayerTheme(themeType = ThemeUiType.LIGHT) {
+// HomeTheme {
+// DetailsContent(
+// state = DetailsViewState(),
+// onItemClick = {}
+// )
+// }
+// }
+//}
+//
+//@Composable
+//@Preview(showBackground = true)
+//private fun DetailsContent_PreviewDark() {
+// MixPlayerTheme(themeType = ThemeUiType.DARK) {
+// HomeTheme {
+// DetailsContent(
+// state = DetailsViewState(),
+// onItemClick = {}
+// )
+// }
+// }
+//}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/DetailsScreen.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/DetailsScreen.kt
new file mode 100644
index 0000000..0cdce6a
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/DetailsScreen.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.presentation.details
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import cafe.adriel.voyager.core.screen.Screen
+import ru.aleshin.core.common.platform.screen.ScreenContent
+import ru.aleshin.features.home.impl.presentation.details.contract.DetailsEffect
+import ru.aleshin.features.home.impl.presentation.details.contract.DetailsEvent
+import ru.aleshin.features.home.impl.presentation.details.contract.DetailsViewState
+import ru.aleshin.features.home.impl.presentation.details.screenmodel.rememberDetailsScreenModel
+import ru.aleshin.features.home.impl.presentation.details.views.DetailsTopBar
+import ru.aleshin.features.home.impl.presentation.mappers.mapToMessage
+import ru.aleshin.features.home.impl.presentation.theme.HomeTheme
+import ru.aleshin.features.home.impl.presentation.theme.HomeThemeRes
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023
+ */
+internal class DetailsScreen @Inject constructor() : Screen {
+
+ @Composable
+ override fun Content() = ScreenContent(
+ screenModel = rememberDetailsScreenModel(),
+ initialState = DetailsViewState(),
+ ) { state ->
+ HomeTheme {
+ val strings = HomeThemeRes.strings
+ val snackbarState = remember { SnackbarHostState() }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ content = { paddingValues ->
+ DetailsContent(
+ state = state,
+ modifier = Modifier.padding(paddingValues),
+ onItemClick = { dispatchEvent(DetailsEvent.PressTrackItem(it)) }
+ )
+ },
+ topBar = {
+ DetailsTopBar(onBackPress = { dispatchEvent(DetailsEvent.PressBackButton) })
+ },
+ snackbarHost = {
+ SnackbarHost(hostState = snackbarState)
+ },
+ )
+
+ handleEffect { effect ->
+ when (effect) {
+ is DetailsEffect.ShowError -> {
+ snackbarState.showSnackbar(
+ message = effect.failures.mapToMessage(strings),
+ withDismissAction = true,
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/contract/DetailsContract.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/contract/DetailsContract.kt
new file mode 100644
index 0000000..934f06c
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/contract/DetailsContract.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.presentation.details.contract
+
+import kotlinx.parcelize.Parcelize
+import ru.aleshin.core.common.functional.audio.AudioInfoUi
+import ru.aleshin.core.common.functional.audio.AudioPlayListUi
+import ru.aleshin.core.common.platform.screenmodel.contract.*
+import ru.aleshin.features.home.impl.domain.entities.HomeFailures
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023
+ */
+@Parcelize
+internal data class DetailsViewState(
+ val isLoading: Boolean = true,
+ val playlist: AudioPlayListUi? = null,
+) : BaseViewState
+
+internal sealed class DetailsEvent : BaseEvent {
+ object Init : DetailsEvent()
+ object PressBackButton : DetailsEvent()
+ data class PressTrackItem(val track: AudioInfoUi) : DetailsEvent()
+}
+
+internal sealed class DetailsEffect : BaseUiEffect {
+ data class ShowError(val failures: HomeFailures) : DetailsEffect()
+}
+
+internal sealed class DetailsAction : BaseAction {
+ object Navigation : DetailsAction()
+ data class UpdateLoading(val isLoading: Boolean) : DetailsAction()
+ data class UpdatePlaylist(val playlist: AudioPlayListUi) : DetailsAction()
+}
diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsEffectCommunicator.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsEffectCommunicator.kt
new file mode 100644
index 0000000..3bf2fb9
--- /dev/null
+++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsEffectCommunicator.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 Stanislav Aleshin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package ru.aleshin.features.home.impl.presentation.details.screenmodel
+
+import ru.aleshin.core.common.platform.communications.state.EffectCommunicator
+import ru.aleshin.features.home.impl.presentation.details.contract.DetailsEffect
+import javax.inject.Inject
+
+/**
+ * @author Stanislav Aleshin on 12.07.2023.
+ */
+internal interface DetailsEffectCommunicator : EffectCommunicator