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 { + + class Base @Inject constructor() : DetailsEffectCommunicator, + EffectCommunicator.Abstract() +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsInfoCommunicator.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsInfoCommunicator.kt new file mode 100644 index 0000000..15d8264 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsInfoCommunicator.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.impl.presentation.details.screenmodel + +import ru.aleshin.core.common.functional.audio.AudioPlayListUi +import ru.aleshin.core.common.platform.communications.Communicator +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.07.2023. + */ +internal interface DetailsInfoCommunicator : Communicator { + class Base @Inject constructor() : DetailsInfoCommunicator, Communicator.AbstractStateFlow(null) +} \ No newline at end of file diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsScreenModel.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsScreenModel.kt new file mode 100644 index 0000000..3485c37 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsScreenModel.kt @@ -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. + */ + +package ru.aleshin.features.home.impl.presentation.details.screenmodel + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import ru.aleshin.core.common.managers.CoroutineManager +import ru.aleshin.core.common.platform.screenmodel.BaseScreenModel +import ru.aleshin.core.common.platform.screenmodel.work.WorkScope +import ru.aleshin.features.home.impl.di.holder.HomeComponentHolder +import ru.aleshin.features.home.impl.presentation.details.contract.DetailsAction +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 javax.inject.Inject + +/** + * @author Stanislav Aleshin on 12.07.2023 + */ +internal class DetailsScreenModel @Inject constructor( + private val detailsWorkProcessor: DetailsWorkProcessor, + stateCommunicator: DetailsStateCommunicator, + effectCommunicator: DetailsEffectCommunicator, + coroutineManager: CoroutineManager, +) : BaseScreenModel( + stateCommunicator = stateCommunicator, + effectCommunicator = effectCommunicator, + coroutineManager = coroutineManager, +) { + + override fun init() { + if (!isInitialize.get()) { + super.init() + dispatchEvent(DetailsEvent.Init) + } + } + + override suspend fun WorkScope.handleEvent( + event: DetailsEvent, + ) { + when (event) { + is DetailsEvent.Init -> launchBackgroundWork(DetailsWorkCommand.LoadPlaylist) { + val command = DetailsWorkCommand.LoadPlaylist + detailsWorkProcessor.work(command).collectAndHandleWork() + } + is DetailsEvent.PressTrackItem -> { + val playlist = checkNotNull(state().playlist) + val command = DetailsWorkCommand.PlayAudio(event.track, playlist) + detailsWorkProcessor.work(command).collectAndHandleWork() + } + is DetailsEvent.PressBackButton -> { + detailsWorkProcessor.work(DetailsWorkCommand.NavigateToBack).collectAndHandleWork() + } + } + } + + override suspend fun reduce( + action: DetailsAction, + currentState: DetailsViewState, + ) = when (action) { + is DetailsAction.Navigation -> currentState.copy( + playlist = null, + ) + is DetailsAction.UpdateLoading -> currentState.copy( + isLoading = action.isLoading + ) + is DetailsAction.UpdatePlaylist -> currentState.copy( + isLoading = false, + playlist = action.playlist, + ) + } +} + +@Composable +internal fun Screen.rememberDetailsScreenModel(): DetailsScreenModel { + val component = HomeComponentHolder.fetchComponent() + return rememberScreenModel { component.fetchDetailsScreenModel() } +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsStateCommunicator.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsStateCommunicator.kt new file mode 100644 index 0000000..9341918 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsStateCommunicator.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.StateCommunicator +import ru.aleshin.features.home.impl.presentation.details.contract.DetailsViewState +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +internal interface DetailsStateCommunicator : StateCommunicator { + + class Base @Inject constructor() : DetailsStateCommunicator, + StateCommunicator.Abstract(defaultState = DetailsViewState()) +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsWorkProcessor.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsWorkProcessor.kt new file mode 100644 index 0000000..930a5b8 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/screenmodel/DetailsWorkProcessor.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.features.home.impl.presentation.details.screenmodel + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import ru.aleshin.core.common.functional.Constants +import ru.aleshin.core.common.functional.audio.AudioInfoUi +import ru.aleshin.core.common.functional.audio.AudioPlayListUi +import ru.aleshin.core.common.platform.screenmodel.work.ActionResult +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.home.impl.navigation.HomeNavigationManager +import ru.aleshin.features.home.impl.presentation.details.contract.DetailsAction +import ru.aleshin.features.home.impl.presentation.details.contract.DetailsEffect +import ru.aleshin.features.player.api.navigation.PlayerScreens +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +internal interface DetailsWorkProcessor : + FlowWorkProcessor { + + class Base @Inject constructor( + private val detailsInfoCommunicator: DetailsInfoCommunicator, + private val navigationManager: HomeNavigationManager, + ) : DetailsWorkProcessor { + + override suspend fun work(command: DetailsWorkCommand) = when (command) { + is DetailsWorkCommand.LoadPlaylist -> loadPlaylistWork() + is DetailsWorkCommand.PlayAudio -> playAudioWork(command.audio, command.playlist) + is DetailsWorkCommand.NavigateToBack -> navigateToBackWork() + } + + + private fun playAudioWork( + audio: AudioInfoUi, + playlists: AudioPlayListUi + ) = flow> { + navigationManager.navigateToPlayer(PlayerScreens.Audio(audio, playlists.listType)) + } + + private suspend fun loadPlaylistWork() = flow { + emit(ActionResult(DetailsAction.UpdateLoading(true))) + detailsInfoCommunicator.collect { details -> + if (details != null) { + delay(Constants.Delay.LOAD_ANIMATION) + emit(ActionResult(DetailsAction.UpdatePlaylist(details))) + emit(ActionResult(DetailsAction.UpdateLoading(false))) + detailsInfoCommunicator.update(null) + } + } + } + + private fun navigateToBackWork() = flow { + emit(ActionResult(DetailsAction.Navigation)) + navigationManager.navigateToLocalBack() + } + } +} + +internal sealed class DetailsWorkCommand : WorkCommand { + object LoadPlaylist : DetailsWorkCommand() + object NavigateToBack : DetailsWorkCommand() + data class PlayAudio(val audio: AudioInfoUi, val playlist: AudioPlayListUi) : + DetailsWorkCommand() +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/views/DetailsTopBar.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/views/DetailsTopBar.kt new file mode 100644 index 0000000..90197e1 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/details/views/DetailsTopBar.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 + * imitations under the License. + */ + +package ru.aleshin.features.home.impl.presentation.details.views + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import ru.aleshin.core.ui.views.TopAppBarEmptyButton +import ru.aleshin.core.ui.views.TopAppBarTitle +import ru.aleshin.features.home.impl.presentation.theme.HomeThemeRes + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +@Composable +@OptIn(ExperimentalMaterial3Api::class) +internal fun DetailsTopBar( + modifier: Modifier = Modifier, + onBackPress: () -> Unit, +) { + TopAppBar( + modifier = modifier, + title = { + TopAppBarTitle(text = HomeThemeRes.strings.detailsHeader) + }, + navigationIcon = { + IconButton(onClick = onBackPress) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = HomeThemeRes.strings.backDesk, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + }, + actions = { + TopAppBarEmptyButton() + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + ), + ) +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/HomeContent.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/HomeContent.kt new file mode 100644 index 0000000..b41a2f4 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/HomeContent.kt @@ -0,0 +1,248 @@ +/* + * 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.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +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.res.painterResource +import androidx.compose.ui.unit.dp +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import ru.aleshin.core.common.extensions.mapAudioPathToPreview +import ru.aleshin.core.common.extensions.mapVideoPathToPreview +import ru.aleshin.core.common.extensions.toSecondsAndMinutesString +import ru.aleshin.core.common.functional.audio.AudioInfoUi +import ru.aleshin.core.common.functional.audio.AudioPlayListUi +import ru.aleshin.core.ui.views.highPlaceholder +import ru.aleshin.core.common.functional.audio.AudioPlayListType +import ru.aleshin.core.common.functional.video.VideoInfoUi +import ru.aleshin.core.common.managers.toImageBitmap +import ru.aleshin.features.home.impl.presentation.home.contract.HomeViewState +import ru.aleshin.features.home.impl.presentation.home.views.HomeSearchBar +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 ru.aleshin.features.home.impl.presentation.home.views.EmptyVideoItem +import ru.aleshin.features.home.impl.presentation.home.views.VideoItem +import ru.aleshin.features.home.impl.presentation.mappers.mapToString +import ru.aleshin.features.home.impl.presentation.theme.HomeThemeRes + +/** + * @author Stanislav Aleshin on 09.07.2023. + */ +@Composable +internal fun HomeContent( + modifier: Modifier = Modifier, + state: HomeViewState, + onSettingsClick: () -> Unit, + onRefreshClick: () -> Unit, + onSearchMedia: (String) -> Unit, + onAudioClick: (AudioInfoUi, AudioPlayListUi) -> Unit, + onVideoClick: (VideoInfoUi) -> Unit, + onMoreSystemAudioPress: (AudioPlayListType) -> Unit, +) { + val scrollState = rememberScrollState() + // Material 3 is not support PullRefresh + val refreshState = rememberSwipeRefreshState(isRefreshing = state.isLoading) + Column { + HomeSearchBar( + modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp), + response = state.searchResponse, + onSearch = onSearchMedia, + onSettingsClick = onSettingsClick, + onChooseAudio = onAudioClick, + onChooseVideo = onVideoClick, + ) + // Material 3 is not support PullRefresh + SwipeRefresh( + state = refreshState, + onRefresh = onRefreshClick + ) { + Column(modifier = modifier.verticalScroll(scrollState)) { + AudioTracksSection( + isLoading = state.isLoading, + playlists = state.playLists, + videos = state.videos, + onChooseAudio = onAudioClick, + onChooseVideo = onVideoClick, + onMorePress = onMoreSystemAudioPress, + ) + } + } + } +} + +@Composable +internal fun AudioTracksSection( + modifier: Modifier = Modifier, + isLoading: Boolean, + playlists: List, + videos: List, + onChooseAudio: (AudioInfoUi, AudioPlayListUi) -> Unit, + onChooseVideo: (VideoInfoUi) -> Unit, + onMorePress: (AudioPlayListType) -> Unit, +) { + if (!isLoading) { + playlists.forEach { playlist -> + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 4.dp, bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = playlist.listType.mapToString(), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + ) + IconButton( + modifier = Modifier.size(28.dp), + onClick = { onMorePress(playlist.listType) }, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(id = HomeThemeRes.icons.more), + contentDescription = HomeThemeRes.strings.moreDesk, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } + val rowState = rememberLazyListState() + LazyRow( + state = rowState, + contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (playlist.audioList.isNotEmpty()) { + items(playlist.audioList, key = { it.id }) { audio -> + AudioItem( + modifier = Modifier.width(170.dp), + image = audio.imagePath?.mapAudioPathToPreview(), + title = audio.title, + authorOrAlbum = audio.artist ?: audio.album ?: "", + duration = audio.duration.toSecondsAndMinutesString(), + onClick = { onChooseAudio(audio, playlist) }, + ) + } + } else { + item(content = { EmptyAudioItem(Modifier.width(170.dp)) }) + } + } + } + } + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 4.dp, bottom = 2.dp), + text = HomeThemeRes.strings.videosHeader, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + ) + val rowState = rememberLazyListState() + LazyRow( + state = rowState, + contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (videos.isNotEmpty()) { + items(videos, key = { it.id }) { video -> + VideoItem( + modifier = Modifier.width(250.dp), + image = video.imagePath?.mapVideoPathToPreview(), + title = video.title, + duration = video.duration.toSecondsAndMinutesString(), + onClick = { onChooseVideo(video) }, + ) + } + } else { + item(content = { EmptyVideoItem(Modifier.width(250.dp)) }) + } + } + } + } else { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { + Box( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 2.dp) + .size(width = 150.dp, height = 30.dp) + .highPlaceholder(shape = MaterialTheme.shapes.medium) + ) + LazyRow( + userScrollEnabled = false, + contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + items(10, itemContent = { AudioItemPlaceholder(Modifier.width(170.dp)) }) + } + } + } +} + +//@Composable +//@Preview(showBackground = true) +//private fun HomeContent_PreviewLight() { +// MixPlayerTheme(themeType = ThemeUiType.LIGHT) { +// HomeTheme { +// HomeContent( +// state = HomeViewState(), +// onSettingsClick = {}, +// onSearchMedia = {}, +// onTrackClick = { _, _ -> }, +// onMoreSystemAudioPress = {}, +// ) +// } +// } +//} +// +//@Composable +//@Preview(showBackground = true) +//private fun HomeContent_PreviewDark() { +// MixPlayerTheme(themeType = ThemeUiType.DARK) { +// HomeTheme { +// HomeContent( +// state = HomeViewState(), +// onSettingsClick = {}, +// onSearchMedia = {}, +// onTrackClick = { _, _ -> }, +// onMoreSystemAudioPress = {}, +// ) +// } +// } +//} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/HomeScreen.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/HomeScreen.kt new file mode 100644 index 0000000..6da700e --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/HomeScreen.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.features.home.impl.presentation.home + +import android.Manifest +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +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.SideEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import cafe.adriel.voyager.core.screen.Screen +import ru.aleshin.core.common.extensions.isAllowPermission +import ru.aleshin.core.common.platform.screen.ScreenContent +import ru.aleshin.core.ui.views.ErrorSnackbar +import ru.aleshin.features.home.impl.presentation.home.contract.HomeEffect +import ru.aleshin.features.home.impl.presentation.home.contract.HomeEvent +import ru.aleshin.features.home.impl.presentation.home.contract.HomeViewState +import ru.aleshin.features.home.impl.presentation.home.screenmodel.rememberHomeScreenModel +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 09.07.2023 + */ +internal class HomeScreen @Inject constructor() : Screen { + + @Composable + override fun Content() = ScreenContent( + screenModel = rememberHomeScreenModel(), + initialState = HomeViewState(), + ) { state -> + HomeTheme { + val context = LocalContext.current + val strings = HomeThemeRes.strings + val snackbarState = remember { SnackbarHostState() } + val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { + if (it) dispatchEvent(HomeEvent.Refresh) + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + content = { paddingValues -> + HomeContent( + state = state, + modifier = Modifier.padding(paddingValues), + onSettingsClick = { dispatchEvent(HomeEvent.PressSettingsButton) }, + onRefreshClick = { dispatchEvent(HomeEvent.Refresh) }, + onSearchMedia = { dispatchEvent(HomeEvent.SearchRequest(it)) }, + onAudioClick = { track, list -> dispatchEvent(HomeEvent.PressAudioItem(track, list)) }, + onVideoClick = { dispatchEvent(HomeEvent.PressVideoItem(it)) }, + onMoreSystemAudioPress = { dispatchEvent(HomeEvent.PressMoreButton(it)) }, + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarState) { + ErrorSnackbar(snackbarData = it) + } + }, + ) + + handleEffect { effect -> + when (effect) { + is HomeEffect.ShowError -> { + snackbarState.showSnackbar( + message = effect.failures.mapToMessage(strings), + withDismissAction = true, + ) + } + } + } + + SideEffect { + if (!context.isAllowPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } + } + } +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/contract/HomeContract.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/contract/HomeContract.kt new file mode 100644 index 0000000..abff11f --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/contract/HomeContract.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.presentation.home.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.core.common.functional.audio.AudioPlayListType +import ru.aleshin.core.common.functional.video.VideoInfoUi +import ru.aleshin.features.home.impl.domain.entities.HomeFailures +import ru.aleshin.features.home.impl.presentation.models.MediaSearchResponse + +/** + * @author Stanislav Aleshin on 17.06.2023 + */ +@Parcelize +internal data class HomeViewState( + val isLoading: Boolean = true, + val playLists: List = emptyList(), + val videos: List = emptyList(), + val searchResponse: MediaSearchResponse? = null, +) : BaseViewState + +internal sealed class HomeEvent : BaseEvent { + object Init : HomeEvent() + object Refresh : HomeEvent() + data class PressMoreButton(val playlistType: AudioPlayListType) : HomeEvent() + data class SearchRequest(val request: String) : HomeEvent() + data class PressAudioItem(val item: AudioInfoUi, val playlist: AudioPlayListUi) : HomeEvent() + data class PressVideoItem(val item: VideoInfoUi) : HomeEvent() + object PressSettingsButton : HomeEvent() +} + +internal sealed class HomeEffect : BaseUiEffect { + data class ShowError(val failures: HomeFailures) : HomeEffect() +} + +internal sealed class HomeAction : BaseAction { + object Navigate : HomeAction() + data class UpdateLoading(val isLoading: Boolean) : HomeAction() + data class UpdateResponse(val searchResponse: MediaSearchResponse?) : HomeAction() + data class UpdateMedia(val playLists: List, val videos: List) : HomeAction() +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeEffectCommunicator.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeEffectCommunicator.kt new file mode 100644 index 0000000..ab58266 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeEffectCommunicator.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.home.screenmodel + +import ru.aleshin.core.common.platform.communications.state.EffectCommunicator +import ru.aleshin.features.home.impl.presentation.home.contract.HomeEffect +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 09.07.2023. + */ +internal interface HomeEffectCommunicator : EffectCommunicator { + + class Base @Inject constructor() : HomeEffectCommunicator, + EffectCommunicator.Abstract() +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeScreenModel.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeScreenModel.kt new file mode 100644 index 0000000..5ac2472 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeScreenModel.kt @@ -0,0 +1,107 @@ +/* + * 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.home.screenmodel + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import ru.aleshin.core.common.managers.CoroutineManager +import ru.aleshin.core.common.platform.screenmodel.BaseScreenModel +import ru.aleshin.core.common.platform.screenmodel.work.WorkScope +import ru.aleshin.features.home.impl.di.holder.HomeComponentHolder +import ru.aleshin.features.home.impl.navigation.HomeNavigationManager +import ru.aleshin.features.home.impl.presentation.home.contract.HomeAction +import ru.aleshin.features.home.impl.presentation.home.contract.HomeEffect +import ru.aleshin.features.home.impl.presentation.home.contract.HomeEvent +import ru.aleshin.features.home.impl.presentation.home.contract.HomeViewState +import ru.aleshin.features.player.api.navigation.PlayerScreens +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 09.07.2023 + */ +internal class HomeScreenModel @Inject constructor( + private val homeWorkProcessor: HomeWorkProcessor, + private val navigationManager: HomeNavigationManager, + stateCommunicator: HomeStateCommunicator, + effectCommunicator: HomeEffectCommunicator, + coroutineManager: CoroutineManager, +) : BaseScreenModel( + stateCommunicator = stateCommunicator, + effectCommunicator = effectCommunicator, + coroutineManager = coroutineManager, +) { + + override fun init() { + if (!isInitialize.get()) { + super.init() + dispatchEvent(HomeEvent.Init) + } + } + + override suspend fun WorkScope.handleEvent( + event: HomeEvent, + ) { + when (event) { + is HomeEvent.Init, HomeEvent.Refresh -> { + sendAction(HomeAction.UpdateLoading(true)) + homeWorkProcessor.work(HomeWorkCommand.LoadTracks).handleWork() + } + is HomeEvent.SearchRequest -> { + val playLists = state().playLists + val videos = state().videos + homeWorkProcessor.work(HomeWorkCommand.SearchRequest(playLists, videos, event.request)).handleWork() + } + is HomeEvent.PressMoreButton -> { + homeWorkProcessor.work(HomeWorkCommand.OpenDetails(event.playlistType, state().playLists)).handleWork() + } + is HomeEvent.PressAudioItem -> { + homeWorkProcessor.work(HomeWorkCommand.OpenAudio(event.item, event.playlist.listType)).handleWork() + } + is HomeEvent.PressVideoItem -> { + homeWorkProcessor.work(HomeWorkCommand.OpenVideo(event.item)).handleWork() + } + is HomeEvent.PressSettingsButton -> navigationManager.navigateToSettings() + } + } + + override suspend fun reduce( + action: HomeAction, + currentState: HomeViewState, + ) = when (action) { + is HomeAction.Navigate -> currentState.copy( + searchResponse = null, + ) + is HomeAction.UpdateLoading -> currentState.copy( + isLoading = action.isLoading, + ) + is HomeAction.UpdateMedia -> currentState.copy( + videos = action.videos, + playLists = action.playLists, + isLoading = false, + ) + is HomeAction.UpdateResponse -> currentState.copy( + searchResponse = action.searchResponse, + ) + } +} + +@Composable +internal fun Screen.rememberHomeScreenModel(): HomeScreenModel { + val component = HomeComponentHolder.fetchComponent() + return rememberScreenModel { component.fetchHomeScreenModel() } +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeStateCommunicator.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeStateCommunicator.kt new file mode 100644 index 0000000..f1ace70 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeStateCommunicator.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.home.screenmodel + +import ru.aleshin.core.common.platform.communications.state.StateCommunicator +import ru.aleshin.features.home.impl.presentation.home.contract.HomeViewState +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 09.07.2023. + */ +internal interface HomeStateCommunicator : StateCommunicator { + + class Base @Inject constructor() : HomeStateCommunicator, + StateCommunicator.Abstract(defaultState = HomeViewState()) +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeWorkProcessor.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeWorkProcessor.kt new file mode 100644 index 0000000..24be231 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/screenmodel/HomeWorkProcessor.kt @@ -0,0 +1,129 @@ +/* + * 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.home.screenmodel + +import ru.aleshin.core.common.functional.Either +import ru.aleshin.core.common.functional.audio.AudioInfoUi +import ru.aleshin.core.common.functional.audio.AudioPlayListUi +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.WorkCommand +import ru.aleshin.core.common.platform.screenmodel.work.WorkProcessor +import ru.aleshin.core.common.platform.screenmodel.work.WorkResult +import ru.aleshin.features.home.api.di.HomeScreens +import ru.aleshin.core.common.functional.audio.AudioPlayListType +import ru.aleshin.core.common.functional.video.VideoInfoUi +import ru.aleshin.features.home.impl.domain.interactors.AudioInteractor +import ru.aleshin.features.home.impl.domain.interactors.VideosInteractor +import ru.aleshin.features.home.impl.navigation.HomeNavigationManager +import ru.aleshin.features.home.impl.presentation.home.contract.HomeAction +import ru.aleshin.features.home.impl.presentation.home.contract.HomeEffect +import ru.aleshin.features.home.impl.presentation.mappers.mapToUi +import ru.aleshin.features.home.impl.presentation.models.MediaSearchResponse +import ru.aleshin.features.player.api.navigation.PlayerScreens +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 11.07.2023. + */ +internal interface HomeWorkProcessor : WorkProcessor { + + class Base @Inject constructor( + private val audioInteractor: AudioInteractor, + private val videosInteractor: VideosInteractor, + private val navigationManager: HomeNavigationManager, + ) : HomeWorkProcessor { + + override suspend fun work(command: HomeWorkCommand) = when (command) { + is HomeWorkCommand.LoadTracks -> loadMediaWork() + is HomeWorkCommand.SearchRequest -> searchRequestWork(command.playLists, command.videos, command.request) + is HomeWorkCommand.OpenDetails -> openDetailsWork(command.type, command.playlists) + is HomeWorkCommand.OpenAudio -> openAudioWork(command.audio, command.type) + is HomeWorkCommand.OpenVideo -> openVideoWork(command.video) + } + + private suspend fun loadMediaWork(): WorkResult { + val videos = videosInteractor.fetchPlaylists().let { videoEither -> + when (videoEither) { + is Either.Right -> videoEither.data.map { it.mapToUi() } + is Either.Left -> return EffectResult(HomeEffect.ShowError(videoEither.data)) + } + } + val playLists = audioInteractor.fetchPlaylists().let { audioEither -> + when (audioEither) { + is Either.Right -> audioEither.data.map { it.mapToUi() } + is Either.Left -> return EffectResult(HomeEffect.ShowError(audioEither.data)) + } + } + return ActionResult(HomeAction.UpdateMedia(playLists, videos)) + } + + private fun searchRequestWork( + playLists: List, + videos: List, + request: String + ): WorkResult { + val filteredPlaylists = mutableListOf().apply { + playLists.forEach { playListModel -> + val tracks = playListModel.audioList.filter { track -> + track.title.contains(request, ignoreCase = true) + } + add(playListModel.copy(audioList = tracks)) + } + } + val filteredVideos = videos.filter { it.title.contains(request, ignoreCase = true) } + val response = MediaSearchResponse(filteredPlaylists, filteredVideos) + return ActionResult(HomeAction.UpdateResponse(searchResponse = response)) + } + + private suspend fun openDetailsWork( + type: AudioPlayListType, + playlists: List + ): WorkResult { + val playlist = playlists.find { it.listType == type } + navigationManager.navigateToLocalScreen(HomeScreens.Details(checkNotNull(playlist))) + return ActionResult(HomeAction.Navigate) + } + + private fun openAudioWork( + audio: AudioInfoUi, + playListType: AudioPlayListType, + ): WorkResult { + navigationManager.navigateToPlayer(PlayerScreens.Audio(audio, playListType)) + return ActionResult(HomeAction.Navigate) + } + + private fun openVideoWork(video: VideoInfoUi): WorkResult { + navigationManager.navigateToPlayer(PlayerScreens.Video(video)) + return ActionResult(HomeAction.Navigate) + } + } +} + +internal sealed class HomeWorkCommand : WorkCommand { + object LoadTracks : HomeWorkCommand() + + data class SearchRequest( + val playLists: List, + val videos: List, + val request: String + ) : HomeWorkCommand() + + data class OpenDetails(val type: AudioPlayListType, val playlists: List) : HomeWorkCommand() + data class OpenAudio(val audio: AudioInfoUi, val type: AudioPlayListType) : HomeWorkCommand() + data class OpenVideo(val video: VideoInfoUi) : HomeWorkCommand() +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/views/AudioItems.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/views/AudioItems.kt new file mode 100644 index 0000000..e32462a --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/views/AudioItems.kt @@ -0,0 +1,165 @@ +/* + * 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.home.views + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Divider +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.ImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import ru.aleshin.core.ui.views.highPlaceholder +import ru.aleshin.features.home.impl.presentation.theme.HomeThemeRes + +/** + * @author Stanislav Aleshin on 13.07.2023. + */ +@Composable +internal fun AudioItem( + modifier: Modifier = Modifier, + image: ImageBitmap?, + title: String, + authorOrAlbum: String, + duration: String, + onClick: () -> Unit, +) { + Box( + modifier = modifier + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .clickable(onClick = onClick) + .height(height = 250.dp), + ) { + Column { + if (image != null) { + Image( + modifier = Modifier + .fillMaxWidth() + .height(160.dp) + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)), + bitmap = image, + contentScale = ContentScale.Crop, + contentDescription = title, + ) + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .height(160.dp) + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(48.dp), + painter = painterResource(id = HomeThemeRes.icons.music), + contentDescription = title, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Column( + modifier = Modifier.padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column( + modifier = Modifier + .padding(horizontal = 12.dp) + .weight(1f) + ) { + Text( + text = authorOrAlbum, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelSmall, + ) + Text( + text = title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + ) + } + Divider() + Column(modifier = Modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + ) { + Text( + modifier = Modifier.weight(1f), + text = duration, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + ) + } + } + } + } + } +} + +@Composable +internal fun EmptyAudioItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .height(height = 250.dp) + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + modifier = Modifier.size(48.dp), + painter = painterResource(id = HomeThemeRes.icons.music), + contentDescription = HomeThemeRes.strings.emptyTracksTitle, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = HomeThemeRes.strings.emptyTracksTitle, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.titleMedium, + ) + } + } +} + +@Composable +internal fun AudioItemPlaceholder(modifier: Modifier = Modifier) = Box( + modifier = modifier.height(250.dp).highPlaceholder(shape = MaterialTheme.shapes.large), +) \ No newline at end of file diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/views/HomeSearchBar.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/views/HomeSearchBar.kt new file mode 100644 index 0000000..b588c34 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/views/HomeSearchBar.kt @@ -0,0 +1,296 @@ +/* + * 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.home.views + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.DockedSearchBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import ru.aleshin.core.common.extensions.mapAudioPathToPreview +import ru.aleshin.core.common.extensions.mapVideoPathToPreview +import ru.aleshin.core.common.extensions.toSecondsAndMinutesString +import ru.aleshin.core.common.functional.audio.AudioInfoUi +import ru.aleshin.core.common.functional.audio.AudioPlayListUi +import ru.aleshin.core.common.functional.video.VideoInfoUi +import ru.aleshin.core.ui.theme.MixPlayerRes +import ru.aleshin.features.home.impl.presentation.models.MediaSearchResponse +import ru.aleshin.features.home.impl.presentation.theme.HomeThemeRes + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +internal fun HomeSearchBar( + modifier: Modifier = Modifier, + isLoading: Boolean = false, + response: MediaSearchResponse?, + onSearch: (String) -> Unit, + onSettingsClick: () -> Unit, + onChooseAudio: (AudioInfoUi, AudioPlayListUi) -> Unit, + onChooseVideo: (VideoInfoUi) -> Unit, +) { + var query by remember { mutableStateOf("") } + var isActive by remember { mutableStateOf(false) } + + DockedSearchBar( + modifier = modifier, + enabled = !isLoading, + query = query, + onQueryChange = { query = it; onSearch(it) }, + onSearch = onSearch, + active = isActive, + onActiveChange = { isActive = it }, + placeholder = { + Text(text = HomeThemeRes.strings.searchPlaceholder) + }, + leadingIcon = { + AnimatedContent( + targetState = isActive, + label = "SearchBarLeadingIcon", + ) { targetState -> + if (targetState) { + IconButton( + modifier = Modifier.size(48.dp), + onClick = { isActive = false; query = "" }, + ) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = HomeThemeRes.strings.closeSearchBarDesk, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } else { + IconButton( + modifier = Modifier.size(48.dp), + onClick = { isActive = true }, + enabled = !isLoading, + ) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = HomeThemeRes.strings.searchPlaceholder, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + }, + trailingIcon = { + AnimatedContent( + targetState = isActive, + label = "SearchBarTrailingIcons", + ) { targetState -> + if (targetState) { + IconButton(modifier = Modifier.size(48.dp), onClick = { query = "" }) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = HomeThemeRes.strings.clearSearchBarDesk, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } else { + IconButton(onClick = onSettingsClick, enabled = !isLoading) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = HomeThemeRes.strings.settingsDesk, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + }, + colors = SearchBarDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ), + ) { + if (response != null) { + LazyColumn { + items(response.videos) { video -> + SearchVideoResponseItem( + image = video.imagePath?.mapVideoPathToPreview(), + title = video.title, + duration = video.duration.toSecondsAndMinutesString(), + onClick = { onChooseVideo(video) }, + ) + } + response.playLists.forEach { playlist -> + items(playlist.audioList) { audio -> + SearchAudioResponseItem( + image = audio.imagePath?.mapAudioPathToPreview(), + title = audio.title, + authorOrAlbum = audio.artist ?: audio.album ?: "", + duration = audio.duration.toSecondsAndMinutesString(), + onClick = { onChooseAudio(audio, playlist) }, + ) + } + } + } + } else { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = HomeThemeRes.icons.emptySearch), + contentDescription = HomeThemeRes.strings.emptySearchTitle, + tint = MaterialTheme.colorScheme.secondary, + ) + Text( + text = HomeThemeRes.strings.emptySearchTitle, + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.labelLarge, + ) + } + } + } + LaunchedEffect(key1 = isActive, block = { query = "" }) +} + +@Composable +internal fun SearchAudioResponseItem( + modifier: Modifier = Modifier, + image: ImageBitmap?, + title: String, + authorOrAlbum: String, + duration: String, + onClick: () -> Unit, +) { + Row( + modifier = modifier.clickable(onClick = onClick).padding(vertical = 8.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (image != null) { + Image( + modifier = Modifier.size(40.dp).clip(MaterialTheme.shapes.small), + bitmap = image, + contentDescription = title, + ) + } else { + Box( + modifier = Modifier + .size(40.dp) + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = HomeThemeRes.icons.music), + contentDescription = title, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = authorOrAlbum, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + } + Text( + text = duration, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelSmall, + ) + } +} + +@Composable +internal fun SearchVideoResponseItem( + modifier: Modifier = Modifier, + image: ImageBitmap?, + title: String, + duration: String, + onClick: () -> Unit, +) { + Row( + modifier = modifier.clickable(onClick = onClick).padding(vertical = 8.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (image != null) { + Image( + modifier = Modifier.size(width = 60.dp, height = 40.dp).clip(MaterialTheme.shapes.small), + bitmap = image, + contentDescription = title, + ) + } else { + Box( + modifier = Modifier + .size(width = 60.dp, height = 40.dp) + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = MixPlayerRes.icons.videos), + contentDescription = title, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + ) + } + Text( + text = duration, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelSmall, + ) + } +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/views/VideoItems.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/views/VideoItems.kt new file mode 100644 index 0000000..e92925e --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/home/views/VideoItems.kt @@ -0,0 +1,148 @@ +/* + * 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.home.views + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +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.ImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import ru.aleshin.core.ui.theme.MixPlayerRes +import ru.aleshin.core.ui.views.highPlaceholder +import ru.aleshin.features.home.impl.presentation.theme.HomeThemeRes + +/** + * @author Stanislav Aleshin on 13.07.2023. + */ +@Composable +internal fun VideoItem( + modifier: Modifier = Modifier, + image: ImageBitmap?, + title: String, + duration: String, + onClick: () -> Unit, +) { + Box( + modifier = modifier + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .clickable(onClick = onClick) + .height(height = 230.dp), + ) { + Column { + if (image != null) { + Image( + modifier = Modifier + .fillMaxWidth() + .height(160.dp) + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)), + bitmap = image, + contentScale = ContentScale.Crop, + contentDescription = title, + ) + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .height(160.dp) + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(48.dp), + painter = painterResource(id = MixPlayerRes.icons.videos), + contentDescription = title, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Column( + modifier = Modifier.padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column( + modifier = Modifier + .padding(horizontal = 12.dp) + .weight(1f) + ) { + Text( + text = HomeThemeRes.strings.videoNameTitle, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelSmall, + ) + Text( + text = title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + maxLines = 2, + ) + } + } + } + } +} + +@Composable +internal fun EmptyVideoItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .height(height = 230.dp) + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + modifier = Modifier.size(48.dp), + painter = painterResource(id = MixPlayerRes.icons.videos), + contentDescription = HomeThemeRes.strings.emptyVideoTitle, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = HomeThemeRes.strings.emptyVideoTitle, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.titleMedium, + ) + } + } +} + +@Composable +internal fun VideoItemPlaceholder(modifier: Modifier = Modifier) = Box( + modifier = modifier.height(230.dp).highPlaceholder(shape = MaterialTheme.shapes.large), +) \ No newline at end of file diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/AudioUiMappers.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/AudioUiMappers.kt new file mode 100644 index 0000000..c00f00f --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/AudioUiMappers.kt @@ -0,0 +1,57 @@ +/* + * 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.mappers + +import ru.aleshin.core.common.functional.audio.AudioInfoUi +import ru.aleshin.core.common.functional.audio.AudioPlayListUi +import ru.aleshin.features.home.api.domain.entities.AudioInfo +import ru.aleshin.features.home.api.domain.entities.AudioPlayList + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +internal fun AudioInfo.mapToUi() = AudioInfoUi( + id = id, + path = path, + title = title, + artist = artist, + album = album, + duration = durationInMillis, + imagePath = imagePath, + date = date, +) + +internal fun AudioPlayList.mapToUi() = AudioPlayListUi( + listType = type, + audioList = audioList.map { it.mapToUi() } +) + +internal fun AudioInfoUi.mapToDomain() = AudioInfo( + id = id, + path = path, + title = title, + artist = artist, + album = album, + imagePath = imagePath, + durationInMillis = duration, + date = date, +) + +internal fun AudioPlayListUi.mapToDomain() = AudioPlayList( + type = listType, + audioList = audioList.map { it.mapToDomain() } +) diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/HomeFailuresMapper.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/HomeFailuresMapper.kt new file mode 100644 index 0000000..3f02a66 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/HomeFailuresMapper.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.impl.presentation.mappers + +import ru.aleshin.features.home.impl.domain.entities.HomeFailures +import ru.aleshin.features.home.impl.presentation.theme.tokens.HomeStrings + +/** + * @author Stanislav Aleshin on 17.06.2023. + */ +internal fun HomeFailures.mapToMessage(string: HomeStrings) = when (this) { + is HomeFailures.OtherError -> string.otherError +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/PlaylistTypeMapper.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/PlaylistTypeMapper.kt new file mode 100644 index 0000000..d4248c6 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/PlaylistTypeMapper.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.presentation.mappers + +import androidx.compose.runtime.Composable +import ru.aleshin.core.common.functional.audio.AudioPlayListType +import ru.aleshin.features.home.impl.presentation.theme.HomeThemeRes + +/** + * @author Stanislav Aleshin on 13.07.2023. + */ +@Composable +internal fun AudioPlayListType.mapToString() = when (this) { + AudioPlayListType.SYSTEM -> HomeThemeRes.strings.systemAudioTitle + AudioPlayListType.APP -> HomeThemeRes.strings.appAudioTitle + AudioPlayListType.OTHER -> "" +} \ No newline at end of file diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/VideoUiMappers.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/VideoUiMappers.kt new file mode 100644 index 0000000..f9a26ff --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/mappers/VideoUiMappers.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.presentation.mappers + +import ru.aleshin.core.common.functional.video.VideoInfoUi +import ru.aleshin.features.home.api.domain.entities.VideoInfo + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +internal fun VideoInfo.mapToUi() = VideoInfoUi( + id = id, + path = path, + title = title, + imagePath = imagePath, + duration = duration, +) \ No newline at end of file diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/models/MediaSearchResponse.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/models/MediaSearchResponse.kt new file mode 100644 index 0000000..84bc89f --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/models/MediaSearchResponse.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.presentation.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import ru.aleshin.core.common.functional.audio.AudioPlayListUi +import ru.aleshin.core.common.functional.video.VideoInfoUi + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +@Parcelize +internal data class MediaSearchResponse( + val playLists: List = emptyList(), + val videos: List = emptyList(), +) : Parcelable diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/nav/NavScreen.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/nav/NavScreen.kt new file mode 100644 index 0000000..9a8f25d --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/nav/NavScreen.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.impl.presentation.nav + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.CurrentScreen +import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior +import ru.aleshin.core.common.navigation.navigator.AppNavigator +import ru.aleshin.core.common.navigation.navigator.rememberNavigatorManager +import ru.aleshin.features.home.impl.di.holder.HomeComponentHolder +import ru.aleshin.features.home.impl.presentation.theme.HomeTheme +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +internal class NavScreen @Inject constructor() : Screen { + + @Composable + override fun Content() = HomeTheme { + AppNavigator( + navigatorManager = rememberNavigatorManager { + HomeComponentHolder.fetchComponent().fetchLocalNavigatorManager() + }, + disposeBehavior = NavigatorDisposeBehavior(false, false), + content = { CurrentScreen() }, + ) + } +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/HomeTheme.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/HomeTheme.kt new file mode 100644 index 0000000..20567ce --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/HomeTheme.kt @@ -0,0 +1,61 @@ +/* + * 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.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.SideEffect +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import ru.aleshin.core.ui.theme.MixPlayerRes +import ru.aleshin.core.ui.theme.tokens.MixPlayerLanguage +import ru.aleshin.features.home.impl.presentation.theme.tokens.HomeIcons +import ru.aleshin.features.home.impl.presentation.theme.tokens.HomeStrings +import ru.aleshin.features.home.impl.presentation.theme.tokens.LocalHomeIcons +import ru.aleshin.features.home.impl.presentation.theme.tokens.LocalHomeStrings + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +@Composable +internal fun HomeTheme(content: @Composable () -> Unit) { + val icons = HomeIcons.DEFAULT + val strings = when (MixPlayerRes.language) { + MixPlayerLanguage.EN -> HomeStrings.ENGLISH + MixPlayerLanguage.RU -> HomeStrings.RUSSIAN + } + + CompositionLocalProvider( + LocalHomeIcons provides icons, + LocalHomeStrings provides strings, + content = content, + ) + HomeSystemUi() +} + +@Composable +private fun HomeSystemUi() { + val systemUiController = rememberSystemUiController() + val navBarColor = MaterialTheme.colorScheme.background + val statusBarColor = MaterialTheme.colorScheme.background + val isDarkIcons = MixPlayerRes.colorsType.isDark + + SideEffect { + systemUiController.setNavigationBarColor(color = navBarColor, darkIcons = !isDarkIcons) + systemUiController.setStatusBarColor(color = statusBarColor, darkIcons = !isDarkIcons) + } +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/HomeThemeRes.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/HomeThemeRes.kt new file mode 100644 index 0000000..d1f0581 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/HomeThemeRes.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.impl.presentation.theme + +import androidx.compose.runtime.Composable +import ru.aleshin.features.home.impl.presentation.theme.tokens.HomeIcons +import ru.aleshin.features.home.impl.presentation.theme.tokens.HomeStrings +import ru.aleshin.features.home.impl.presentation.theme.tokens.LocalHomeIcons +import ru.aleshin.features.home.impl.presentation.theme.tokens.LocalHomeStrings + +/** + * @author Stanislav Aleshin on 14.06.2023 + */ +internal object HomeThemeRes { + + val icons: HomeIcons + @Composable get() = LocalHomeIcons.current + + val strings: HomeStrings + @Composable get() = LocalHomeStrings.current +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/tokens/HomeIcons.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/tokens/HomeIcons.kt new file mode 100644 index 0000000..cf47454 --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/tokens/HomeIcons.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.features.home.impl.presentation.theme.tokens + +import androidx.compose.runtime.staticCompositionLocalOf +import ru.aleshin.features.home.impl.R + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +internal data class HomeIcons( + val emptySearch: Int, + val emptyList: Int, + val music: Int, + val more: Int, +) { + companion object { + val DEFAULT = HomeIcons( + emptySearch = R.drawable.ic_content_search, + emptyList = R.drawable.ic_magnify_scan, + music = R.drawable.ic_music, + more = R.drawable.ic_more, + ) + } +} + +internal val LocalHomeIcons = staticCompositionLocalOf { + error("Home Icons is not provided") +} diff --git a/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/tokens/HomeStrings.kt b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/tokens/HomeStrings.kt new file mode 100644 index 0000000..657902f --- /dev/null +++ b/features/home/impl/src/main/java/ru/aleshin/features/home/impl/presentation/theme/tokens/HomeStrings.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.features.home.impl.presentation.theme.tokens + +import androidx.compose.runtime.staticCompositionLocalOf + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +internal data class HomeStrings( + val otherError: String, + val searchPlaceholder: String, + val settingsDesk: String, + val menuDesk: String, + val moreDesk: String, + val systemAudioTitle: String, + val appAudioTitle: String, + val detailsHeader: String, + val closeSearchBarDesk: String, + val clearSearchBarDesk: String, + val emptySearchTitle: String, + val emptyTracksTitle: String, + val emptyVideoTitle: String, + val backDesk: String, + val videosHeader: String, + val videoNameTitle: String, +) { + companion object { + val RUSSIAN = HomeStrings( + otherError = "Ошибка! Обратитесь к разработчику!", + searchPlaceholder = "Поиск медиа", + settingsDesk = "Настройки", + menuDesk = "Меню", + moreDesk = "Больше треков", + systemAudioTitle = "Загруженные треки", + appAudioTitle = "Треки приложения", + detailsHeader = "Треки", + closeSearchBarDesk = "Закрыть", + clearSearchBarDesk = "Очстить", + emptySearchTitle = "Нет результатов", + emptyTracksTitle = "Треков нет", + emptyVideoTitle = "Видео роликов нет", + backDesk = "Назад", + videosHeader = "Видео ролики", + videoNameTitle = "Название", + ) + val ENGLISH = HomeStrings( + otherError = "Error! Contact the developer!", + searchPlaceholder = "Media Search", + settingsDesk = "Settings", + menuDesk = "Menu", + moreDesk = "More tracks", + systemAudioTitle = "Uploaded tracks", + appAudioTitle = "App Tracks", + detailsHeader = "Tracks", + closeSearchBarDesk = "Close", + clearSearchBarDesk = "Clear", + emptySearchTitle = "No results", + emptyTracksTitle = "No tracks", + emptyVideoTitle = "No videos", + backDesk = "Back", + videosHeader = "Videos", + videoNameTitle = "Title", + ) + } +} + +internal val LocalHomeStrings = staticCompositionLocalOf { + error("Home Strings is not provided") +} diff --git a/features/home/impl/src/main/res/drawable/ic_content_search.xml b/features/home/impl/src/main/res/drawable/ic_content_search.xml new file mode 100644 index 0000000..acc78f0 --- /dev/null +++ b/features/home/impl/src/main/res/drawable/ic_content_search.xml @@ -0,0 +1,6 @@ + + + + diff --git a/features/home/impl/src/main/res/drawable/ic_magnify_scan.xml b/features/home/impl/src/main/res/drawable/ic_magnify_scan.xml new file mode 100644 index 0000000..75b1a9d --- /dev/null +++ b/features/home/impl/src/main/res/drawable/ic_magnify_scan.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/home/impl/src/main/res/drawable/ic_more.xml b/features/home/impl/src/main/res/drawable/ic_more.xml new file mode 100644 index 0000000..afbe22d --- /dev/null +++ b/features/home/impl/src/main/res/drawable/ic_more.xml @@ -0,0 +1,5 @@ + + + diff --git a/features/home/impl/src/main/res/drawable/ic_music.xml b/features/home/impl/src/main/res/drawable/ic_music.xml new file mode 100644 index 0000000..4e7f409 --- /dev/null +++ b/features/home/impl/src/main/res/drawable/ic_music.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/home/impl/src/test/java/ru/aleshin/features/home/impl/ExampleUnitTest.kt b/features/home/impl/src/test/java/ru/aleshin/features/home/impl/ExampleUnitTest.kt new file mode 100644 index 0000000..25707f2 --- /dev/null +++ b/features/home/impl/src/test/java/ru/aleshin/features/home/impl/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.impl + +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/player/api/.gitignore b/features/player/api/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/features/player/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/player/api/build.gradle.kts b/features/player/api/build.gradle.kts new file mode 100644 index 0000000..d7405d8 --- /dev/null +++ b/features/player/api/build.gradle.kts @@ -0,0 +1,102 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + +/* + * 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 { + val localProperties = gradleLocalProperties(rootDir) + + namespace = "ru.aleshin.features.player.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 + 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(Dependencies.Voyager.navigator) + + implementation(Dependencies.AndroidX.core) + implementation(Dependencies.AndroidX.appcompat) + implementation(Dependencies.AndroidX.material) + implementation(Dependencies.AndroidX.gson) + + implementation(Dependencies.ExoPlayer.library) + implementation(Dependencies.ExoPlayer.ui) + + 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/player/api/consumer-rules.pro b/features/player/api/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/features/player/api/proguard-rules.pro b/features/player/api/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/features/player/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/player/api/src/androidTest/java/ru/aleshin/features/authoriztion/api/ExampleInstrumentedTest.kt b/features/player/api/src/androidTest/java/ru/aleshin/features/authoriztion/api/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..c4aa46d --- /dev/null +++ b/features/player/api/src/androidTest/java/ru/aleshin/features/authoriztion/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.authoriztion.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.authoriztion.api.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/features/player/api/src/main/AndroidManifest.xml b/features/player/api/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a625e10 --- /dev/null +++ b/features/player/api/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/di/PlayerFeatureApi.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/di/PlayerFeatureApi.kt new file mode 100644 index 0000000..4015798 --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/di/PlayerFeatureApi.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.player.api.di + +import ru.aleshin.features.player.api.navigation.PlayerFeatureStarter +import ru.aleshin.module_injector.BaseFeatureApi + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +interface PlayerFeatureApi : BaseFeatureApi { + fun fetchStarter(): PlayerFeatureStarter +} diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/navigation/PlayerFeatureStarter.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/navigation/PlayerFeatureStarter.kt new file mode 100644 index 0000000..5fa5fd5 --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/navigation/PlayerFeatureStarter.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.player.api.navigation + +import cafe.adriel.voyager.core.screen.Screen + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +interface PlayerFeatureStarter { + fun fetchPlayerScreen(navScreen: PlayerScreens): Screen +} diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/navigation/PlayerScreens.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/navigation/PlayerScreens.kt new file mode 100644 index 0000000..aba8d33 --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/navigation/PlayerScreens.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.player.api.navigation + +import ru.aleshin.core.common.functional.audio.AudioInfoUi +import ru.aleshin.core.common.functional.audio.AudioPlayListType +import ru.aleshin.core.common.functional.audio.AudioPlayListUi +import ru.aleshin.core.common.functional.video.VideoInfoUi + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +sealed class PlayerScreens { + data class Audio(val audio: AudioInfoUi, val playListType: AudioPlayListType) : PlayerScreens() + data class Video(val videoInfoModel: VideoInfoUi) : PlayerScreens() +} diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/MediaCommunicator.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/MediaCommunicator.kt new file mode 100644 index 0000000..c218e11 --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/MediaCommunicator.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.player.api.presentation.common + +import ru.aleshin.core.common.functional.MediaCommand +import ru.aleshin.core.common.platform.communications.Communicator +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 14.07.2023. + */ +interface MediaCommunicator : Communicator { + class Base @Inject constructor() : MediaCommunicator, Communicator.AbstractSharedFlow( + flowReplay = 0, + flowBufferCapacity = 1, + ) +} \ No newline at end of file diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/MediaController.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/MediaController.kt new file mode 100644 index 0000000..d5ac198 --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/MediaController.kt @@ -0,0 +1,41 @@ +/* + * 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.player.api.presentation.common + +import kotlinx.coroutines.flow.FlowCollector +import ru.aleshin.core.common.functional.MediaCommand +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 14.07.2023. + */ +interface MediaController { + + suspend fun collectCommands(collector: FlowCollector) + fun work(command: MediaCommand) + + class Base @Inject constructor( + private val mediaCommunicator: MediaCommunicator + ) : MediaController { + + override fun work(command: MediaCommand) = mediaCommunicator.update(command) + + override suspend fun collectCommands( + collector: FlowCollector + ) = mediaCommunicator.collect(collector) + } +} \ No newline at end of file diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/PlaybackCommunicator.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/PlaybackCommunicator.kt new file mode 100644 index 0000000..6515fe2 --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/PlaybackCommunicator.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.player.api.presentation.common + +import ru.aleshin.core.common.functional.audio.PlayerInfo +import ru.aleshin.core.common.platform.communications.Communicator +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 14.07.2023. + */ +interface PlaybackCommunicator : Communicator { + class Base @Inject constructor() : PlaybackCommunicator, Communicator.AbstractSharedFlow( + flowReplay = 1, + ) +} \ No newline at end of file diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/PlaybackManager.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/PlaybackManager.kt new file mode 100644 index 0000000..c08ae34 --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/common/PlaybackManager.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.player.api.presentation.common + +import kotlinx.coroutines.flow.FlowCollector +import ru.aleshin.core.common.functional.audio.PlayerInfo +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 14.07.2023. + */ +interface PlaybackManager { + + suspend fun collectInfo(collector: FlowCollector) + fun sendInfo(info: PlayerInfo) + + class Base @Inject constructor( + private val playbackCommunicator: PlaybackCommunicator + ) : PlaybackManager { + + override fun sendInfo(info: PlayerInfo) = playbackCommunicator.update(info) + + override suspend fun collectInfo( + collector: FlowCollector + ) = playbackCommunicator.collect(collector) + } + +} \ No newline at end of file diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/MediaPlayerManager.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/MediaPlayerManager.kt new file mode 100644 index 0000000..bbdb823 --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/MediaPlayerManager.kt @@ -0,0 +1,142 @@ +/* + * 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.player.api.presentation.player + +import android.content.Context +import android.media.MediaPlayer +import android.net.Uri +import android.util.Log +import ru.aleshin.core.common.functional.audio.AudioInfoUi +import ru.aleshin.core.common.functional.audio.AudioPlayListType +import ru.aleshin.core.common.functional.audio.AudioPlayListUi +import ru.aleshin.core.common.functional.audio.PlaybackInfoUi +import ru.aleshin.core.common.functional.audio.PlayerError + +/** + * @author Stanislav Aleshin on 13.07.2023. + */ +interface MediaPlayerManager : MediaPlayerVolume, MediaPlayerListeners { + + fun setAudio(audio: AudioInfoUi, type: AudioPlayListType) + fun play() + fun pause() + fun stop() + fun release() + fun seekTo(position: Int) + fun fetchPosition(): Int + fun isPlaying(): Boolean + + class Base( + private val context: Context, + private val store: PlayerInfoStore + ) : MediaPlayerManager { + + private var volume: Float = 1f + private var mediaPlayer: MediaPlayer? = prepareMediaPlayer() + + override fun setAudio(audio: AudioInfoUi, type: AudioPlayListType) = try { + if (audio.id != store.fetchInfo().playback.currentAudio?.id) { + mediaPlayer?.reset() + mediaPlayer?.setDataSource(context, Uri.parse(audio.path)) + mediaPlayer?.prepare() + store.updateInfo { + copy( + playback = playback.copy( + currentAudio = audio, + position = 0, + isPlay = false, + isComplete = false, + ), + playListType = type, + ) + } + } else { + Unit + } + } catch (e: Exception) { + store.sendError(PlayerError.DATA_SOURCE) + } + + override fun setVolume(value: Float) = store.updateInfo { + volume = value + mediaPlayer?.setVolume(volume, volume) + copy(playback = playback.copy(volume = value)) + } + + override fun fetchVolume(): Float { + return volume + } + + override fun onPrepared(player: MediaPlayer?) = play() + + override fun onCompletion(player: MediaPlayer?) = stop() + + override fun onError(player: MediaPlayer?, error: Int, extra: Int): Boolean { + store.sendError(PlayerError.OTHER) + mediaPlayer?.release() + mediaPlayer = prepareMediaPlayer() + return true + } + + override fun play() = store.updateInfo { + mediaPlayer?.start() + copy(playback = playback.copy(isPlay = true)) + } + + override fun pause() = store.updateInfo { + mediaPlayer?.pause() + copy(playback = playback.copy(isPlay = false)) + } + + override fun stop() = store.updateInfo { + mediaPlayer?.stop() + copy(playback = playback.copy(isPlay = false, isComplete = true)) + } + + override fun seekTo(position: Int) = store.updateInfo { + mediaPlayer?.seekTo(position) + copy(playback = playback.copy(position = position)) + } + + override fun fetchPosition(): Int { + return mediaPlayer?.currentPosition ?: -1 + } + + override fun isPlaying(): Boolean { + return mediaPlayer?.isPlaying ?: false + } + + override fun release() = store.updateInfo { + mediaPlayer?.release() + mediaPlayer = null + copy(playback = PlaybackInfoUi(volume = volume)) + } + + private fun prepareMediaPlayer() = MediaPlayer().apply { + store.updateInfo { copy(playback = PlaybackInfoUi(volume = volume)) } + setVolume(volume, volume) + setOnErrorListener(this@Base) + setOnCompletionListener(this@Base) + setOnPreparedListener(this@Base) + } + } +} + +interface MediaPlayerListeners : + MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, + MediaPlayer.OnErrorListener \ No newline at end of file diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/MediaPlayerVolume.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/MediaPlayerVolume.kt new file mode 100644 index 0000000..560403b --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/MediaPlayerVolume.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.player.api.presentation.player + +/** + * @author Stanislav Aleshin on 15.07.2023. + */ +interface MediaPlayerVolume { + fun setVolume(value: Float) + fun fetchVolume(): Float +} \ No newline at end of file diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/PlayerInfoStore.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/PlayerInfoStore.kt new file mode 100644 index 0000000..3c94acb --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/PlayerInfoStore.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 + * imitations under the License. + */ + +package ru.aleshin.features.player.api.presentation.player + +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import ru.aleshin.core.common.functional.audio.PlayerError +import ru.aleshin.core.common.functional.audio.PlayerInfo +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 13.07.2023. + */ +interface PlayerInfoStore : PlayerInfoStoreCollect { + + fun fetchInfo(): PlayerInfo + fun updateInfo(builder: PlayerInfo.() -> PlayerInfo) + fun sendError(error: PlayerError) + + class Base @Inject constructor( + private val infoFlow: MutableStateFlow = MutableStateFlow(PlayerInfo()), + private val errorsFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1), + ) : PlayerInfoStore { + + private var info = PlayerInfo() + + override fun fetchInfo(): PlayerInfo { + return info + } + + override fun updateInfo(builder: PlayerInfo.() -> PlayerInfo) { + info = builder(info).apply { infoFlow.tryEmit(this) } + } + + override fun sendError(error: PlayerError) { + errorsFlow.tryEmit(error) + } + + override suspend fun collectInfo(collector: FlowCollector) = infoFlow.collect(collector) + + override suspend fun collectErrors(collector: FlowCollector) = errorsFlow.collect(collector) + } +} + +interface PlayerInfoStoreCollect { + suspend fun collectInfo(collector: FlowCollector) + suspend fun collectErrors(collector: FlowCollector) +} \ No newline at end of file diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/PlayerService.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/PlayerService.kt new file mode 100644 index 0000000..6ab5e2e --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/PlayerService.kt @@ -0,0 +1,164 @@ +/* + * 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.player.api.presentation.player + +import android.R +import android.app.Notification +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.AudioManager.OnAudioFocusChangeListener +import android.os.IBinder +import androidx.core.app.NotificationCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import ru.aleshin.core.common.functional.Constants +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.managers.AudioManagerController +import ru.aleshin.core.common.notifications.NotificationCreator + + +/** + * @author Stanislav Aleshin on 13.07.2023. + */ +abstract class PlayerServiceAbstract : Service(), PlayerInfoStoreCollect, OnAudioFocusChangeListener { + abstract fun workMediaCommand(mediaCommand: MediaCommand) +} + +class PlayerService : PlayerServiceAbstract() { + + private val job = SupervisorJob() + private val scope = CoroutineScope(job) + + private lateinit var audioManager: AudioManager + private lateinit var audioManagerController: AudioManagerController + private lateinit var mediaPlayerManager: MediaPlayerManager + private lateinit var playerStore: PlayerInfoStore + + private val binder by lazy { PlayerServiceBinder(this) } + + private val audioAttributes = AudioAttributes.Builder().let { + it.setUsage(AudioAttributes.USAGE_MEDIA) + it.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + it.build() + } + + override fun onCreate() { + super.onCreate() + audioManager = getSystemService(AudioManager::class.java) + playerStore = PlayerInfoStore.Base() + mediaPlayerManager = MediaPlayerManager.Base(this, playerStore) + audioManagerController = AudioManagerController.Base(audioManager, audioAttributes) + audioManagerController.captureAudioFocus(this) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startForeground(Constants.Notification.FOREGROUND_NOTIFY_ID, createNotification()) + + when (intent?.action) { + Constants.Notification.ACTION_PLAY_PAUSE -> workMediaCommand(MediaCommand.PlayOrPause) + } + + return super.onStartCommand(intent, flags, startId) + } + + override fun onBind(p0: Intent?): IBinder { + updatePosition() + return binder + } + + override fun onUnbind(intent: Intent?): Boolean { + return true + } + + override fun onDestroy() { + super.onDestroy() + job.cancel() + audioManagerController.freeAudioFocus(this) + mediaPlayerManager.release() + stopForeground(STOP_FOREGROUND_REMOVE) + } + + override fun workMediaCommand(mediaCommand: MediaCommand): Unit = with(mediaPlayerManager) { + when (mediaCommand) { + is MediaCommand.SelectAudio -> setAudio(mediaCommand.audio, mediaCommand.type) + is MediaCommand.PlayOrPause -> if (isPlaying()) pause() else play() + is MediaCommand.SeekTo -> seekTo(mediaCommand.value) + is MediaCommand.ChangeVolume -> setVolume(mediaCommand.value) + } + } + + override fun onAudioFocusChange(focus: Int) = with(mediaPlayerManager) { + when (focus) { + AudioManager.AUDIOFOCUS_GAIN -> { + if (!isPlaying()) play() + setVolume(fetchVolume()) + } + AudioManager.AUDIOFOCUS_LOSS -> { + if (isPlaying()) stop() + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + if (isPlaying()) pause() + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + if (isPlaying()) setVolume(0.1f) + } + } + } + + private fun createNotification(): Notification { + val playIntent = Intent(this, PlayerService::class.java).apply { + action = Constants.Notification.ACTION_PLAY_PAUSE + } + val pendingPlay = PendingIntent.getService(this, 0, playIntent, PendingIntent.FLAG_IMMUTABLE) + + val actions = listOf( + NotificationCompat.Action.Builder(R.drawable.ic_media_play, "Плей/Пауза", pendingPlay).build(), + ) + + return NotificationCreator.Base(this).createNotify( + channelId = Constants.Notification.CHANNEL_ID, + title = "MixPlayer", + text = "Play audio music", + smallIcon = R.drawable.ic_media_play, + ongoing = true, + actions = actions + ) + } + + private fun updatePosition() = scope.launch { + while (isActive) { + if (mediaPlayerManager.isPlaying()) { + val position = mediaPlayerManager.fetchPosition() + playerStore.updateInfo { copy(playback = playback.copy(position = position)) } + } + delay(Constants.Delay.SLIDER_POSITION_UPDATE) + } + } + + override suspend fun collectErrors(collector: FlowCollector) = playerStore.collectErrors(collector) + + override suspend fun collectInfo(collector: FlowCollector) = playerStore.collectInfo(collector) +} \ No newline at end of file diff --git a/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/PlayerServiceBinder.kt b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/PlayerServiceBinder.kt new file mode 100644 index 0000000..ced772b --- /dev/null +++ b/features/player/api/src/main/java/ru/aleshin/features/player/api/presentation/player/PlayerServiceBinder.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 + * imitations under the License. + */ + +package ru.aleshin.features.player.api.presentation.player + +import ru.aleshin.core.common.services.ServiceBinder + +class PlayerServiceBinder(service: PlayerService) : ServiceBinder.Abstract(service) \ No newline at end of file diff --git a/features/player/api/src/test/java/ru/aleshin/features/player/api/ExampleUnitTest.kt b/features/player/api/src/test/java/ru/aleshin/features/player/api/ExampleUnitTest.kt new file mode 100644 index 0000000..629b975 --- /dev/null +++ b/features/player/api/src/test/java/ru/aleshin/features/player/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.authoriztion.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/player/impl/.gitignore b/features/player/impl/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/features/player/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/player/impl/build.gradle.kts b/features/player/impl/build.gradle.kts new file mode 100644 index 0000000..3a96ab9 --- /dev/null +++ b/features/player/impl/build.gradle.kts @@ -0,0 +1,110 @@ +/* + * 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.player.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:player:api")) + implementation(project(":features:settings:api")) + implementation(project(":features:home: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.Compose.ui) + implementation(Dependencies.Compose.activity) + + implementation(Dependencies.ExoPlayer.library) + implementation(Dependencies.ExoPlayer.ui) + + 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/player/impl/consumer-rules.pro b/features/player/impl/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/features/player/impl/proguard-rules.pro b/features/player/impl/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/features/player/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/player/impl/src/androidTest/java/ru/aleshin/features/authoriztion/impl/ExampleInstrumentedTest.kt b/features/player/impl/src/androidTest/java/ru/aleshin/features/authoriztion/impl/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d1694ab --- /dev/null +++ b/features/player/impl/src/androidTest/java/ru/aleshin/features/authoriztion/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.authoriztion.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.authoriztion.impl.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/features/player/impl/src/main/AndroidManifest.xml b/features/player/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c193aa3 --- /dev/null +++ b/features/player/impl/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/PlayerFeatureDependencies.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/PlayerFeatureDependencies.kt new file mode 100644 index 0000000..0616d55 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/PlayerFeatureDependencies.kt @@ -0,0 +1,41 @@ +/* + * 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.player.impl.di + +import ru.aleshin.core.common.managers.CoroutineManager +import ru.aleshin.core.common.navigation.Router +import ru.aleshin.features.home.api.domain.repositories.AppAudioRepository +import ru.aleshin.features.home.api.domain.repositories.SystemAudioRepository +import ru.aleshin.features.home.api.navigation.HomeFeatureStarter +import ru.aleshin.features.player.api.presentation.common.MediaController +import ru.aleshin.features.player.api.presentation.common.PlaybackManager +import ru.aleshin.features.settings.api.domain.repositories.SettingsRepository +import ru.aleshin.module_injector.BaseFeatureDependencies + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +interface PlayerFeatureDependencies : BaseFeatureDependencies { + val appAudioRepository: AppAudioRepository + val systemAudioRepository: SystemAudioRepository + val settingsRepository: SettingsRepository + val playbackManager: PlaybackManager + val mediaController: MediaController + val homeFeatureStarter: HomeFeatureStarter + val coroutineManager: CoroutineManager + val globalRouter: Router +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/component/PlayerComponent.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/component/PlayerComponent.kt new file mode 100644 index 0000000..41ff8b2 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/component/PlayerComponent.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 + * imitations under the License. + */ + +package ru.aleshin.features.player.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.player.api.di.PlayerFeatureApi +import ru.aleshin.features.player.impl.di.PlayerFeatureDependencies +import ru.aleshin.features.player.impl.di.modules.DataModule +import ru.aleshin.features.player.impl.di.modules.DomainModule +import ru.aleshin.features.player.impl.di.modules.NavigationModule +import ru.aleshin.features.player.impl.di.modules.PresentationModule +import ru.aleshin.features.player.impl.presentation.audio.screenmodel.AudioScreenModel +import ru.aleshin.features.player.impl.presentation.video.screenmodel.VideoScreenModel + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +@FeatureScope +@Component( + modules = [ + DataModule::class, + DomainModule::class, + PresentationModule::class, + NavigationModule::class, + ], + dependencies = [PlayerFeatureDependencies::class], +) +internal interface PlayerComponent : PlayerFeatureApi { + + fun fetchLocalNavigatorManager(): NavigatorManager + fun fetchAudioScreenModel(): AudioScreenModel + fun fetchVideoScreenModel(): VideoScreenModel + + @Component.Builder + interface Builder { + fun dependencies(deps: PlayerFeatureDependencies): Builder + fun build(): PlayerComponent + } + + companion object { + fun create(deps: PlayerFeatureDependencies): PlayerComponent { + return DaggerPlayerComponent.builder() + .dependencies(deps) + .build() + } + } +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/holder/PlayerComponentHolder.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/holder/PlayerComponentHolder.kt new file mode 100644 index 0000000..70d834b --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/holder/PlayerComponentHolder.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.player.impl.di.holder + +import ru.aleshin.features.player.api.di.PlayerFeatureApi +import ru.aleshin.features.player.impl.di.PlayerFeatureDependencies +import ru.aleshin.features.player.impl.di.component.PlayerComponent +import ru.aleshin.module_injector.BaseComponentHolder + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +object PlayerComponentHolder : BaseComponentHolder { + + private var component: PlayerComponent? = null + + override fun init(dependencies: PlayerFeatureDependencies) { + if (component == null) component = PlayerComponent.create(dependencies) + } + + override fun clear() { + component = null + } + + override fun fetchApi(): PlayerFeatureApi { + return fetchComponent() + } + + internal fun fetchComponent() = checkNotNull(component) { + "PlayerComponent is not initialized" + } +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/modules/DataModule.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/modules/DataModule.kt new file mode 100644 index 0000000..02d0397 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/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.player.impl.di.modules + +import dagger.Module + +/** + * @author Stanislav Aleshin on 11.07.2023. + */ +@Module +internal interface DataModule diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/modules/DomainModule.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/modules/DomainModule.kt new file mode 100644 index 0000000..44b1fb1 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/modules/DomainModule.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.player.impl.di.modules + +import dagger.Binds +import dagger.Module +import ru.aleshin.core.common.di.FeatureScope +import ru.aleshin.features.player.impl.domain.common.PlayerEitherWrapper +import ru.aleshin.features.player.impl.domain.common.PlayerErrorHandler +import ru.aleshin.features.player.impl.domain.interactors.AudioInteractor +import ru.aleshin.features.player.impl.domain.interactors.PlayerSettingsInteractor + +/** + * @author Stanislav Aleshin on 05.07.2023. + */ +@Module +internal interface DomainModule { + + @Binds + @FeatureScope + fun bindAuthErrorHandler(handler: PlayerErrorHandler.Base): PlayerErrorHandler + + @Binds + @FeatureScope + fun bindAuthEiterWrapper(wrapper: PlayerEitherWrapper.Base): PlayerEitherWrapper + + @Binds + fun bindPlayerSettingsInteractor(interactor: PlayerSettingsInteractor.Base): PlayerSettingsInteractor + + @Binds + fun bindAudioInteractor(interactor: AudioInteractor.Base): AudioInteractor +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/modules/NavigationModule.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/modules/NavigationModule.kt new file mode 100644 index 0000000..541d413 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/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.player.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 15.06.2023. + */ +@Module +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/player/impl/src/main/java/ru/aleshin/features/player/impl/di/modules/PresentationModule.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/modules/PresentationModule.kt new file mode 100644 index 0000000..2c39c0c --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/di/modules/PresentationModule.kt @@ -0,0 +1,97 @@ +/* + * 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.player.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.player.api.navigation.PlayerFeatureStarter +import ru.aleshin.features.player.impl.navigation.PlayerNavigationManager +import ru.aleshin.features.player.impl.navigation.PlayerFeatureStarterImpl +import ru.aleshin.features.player.impl.presentation.audio.screenmodel.AudioEffectCommunicator +import ru.aleshin.features.player.impl.presentation.audio.screenmodel.AudioInfoCommunicator +import ru.aleshin.features.player.impl.presentation.audio.screenmodel.AudioScreenModel +import ru.aleshin.features.player.impl.presentation.audio.screenmodel.AudioStateCommunicator +import ru.aleshin.features.player.impl.presentation.audio.screenmodel.AudioWorkProcessor +import ru.aleshin.features.player.impl.presentation.nav.NavScreen +import ru.aleshin.features.player.impl.presentation.video.screenmodel.VideoEffectCommunicator +import ru.aleshin.features.player.impl.presentation.video.screenmodel.VideoInfoCommunicator +import ru.aleshin.features.player.impl.presentation.video.screenmodel.VideoScreenModel +import ru.aleshin.features.player.impl.presentation.video.screenmodel.VideoStateCommunicator +import ru.aleshin.features.player.impl.presentation.video.screenmodel.VideoWorkProcessor + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +@Module +internal interface PresentationModule { + + // Common + + @Binds + @FeatureScope + fun bindAuthFeatureStarter(starter: PlayerFeatureStarterImpl): PlayerFeatureStarter + + @Binds + @FeatureScope + fun bindLocalNavigationManager(manager: PlayerNavigationManager.Base): PlayerNavigationManager + + @Binds + @FeatureScope + fun bindNavScreen(screen: NavScreen): Screen + + // Audio + + @Binds + @ScreenModelKey(AudioScreenModel::class) + fun bindAudioScreenModel(screenModel: AudioScreenModel): ScreenModel + + @Binds + fun bindAudioStateCommunicator(communicator: AudioStateCommunicator.Base): AudioStateCommunicator + + @Binds + fun bindAudioEffectCommunicator(communicator: AudioEffectCommunicator.Base): AudioEffectCommunicator + + @Binds + @FeatureScope + fun bindAudioInfoCommunicator(communicator: AudioInfoCommunicator.Base): AudioInfoCommunicator + + @Binds + fun bindAudioWorkProcessor(workProcessor: AudioWorkProcessor.Base): AudioWorkProcessor + + // Video + + @Binds + @ScreenModelKey(VideoScreenModel::class) + fun bindVideoScreenModel(screenModel: VideoScreenModel): ScreenModel + + @Binds + fun bindVideoStateCommunicator(communicator: VideoStateCommunicator.Base): VideoStateCommunicator + + @Binds + fun bindVideoEffectCommunicator(communicator: VideoEffectCommunicator.Base): VideoEffectCommunicator + + @Binds + @FeatureScope + fun bindVideoInfoCommunicator(communicator: VideoInfoCommunicator.Base): VideoInfoCommunicator + + @Binds + fun bindVideoWorkProcessor(workProcessor: VideoWorkProcessor.Base): VideoWorkProcessor +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/common/PlayerEitherWrapper.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/common/PlayerEitherWrapper.kt new file mode 100644 index 0000000..24ff04f --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/common/PlayerEitherWrapper.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.player.impl.domain.common + +import ru.aleshin.core.common.wrappers.EitherWrapper +import ru.aleshin.features.player.impl.domain.entities.PlayerFailures +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +internal interface PlayerEitherWrapper : EitherWrapper { + + class Base @Inject constructor( + errorHandler: PlayerErrorHandler, + ) : PlayerEitherWrapper, EitherWrapper.Abstract(errorHandler) +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/common/PlayerErrorHandler.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/common/PlayerErrorHandler.kt new file mode 100644 index 0000000..fbd7c22 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/common/PlayerErrorHandler.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.player.impl.domain.common + +import ru.aleshin.core.common.handlers.ErrorHandler +import ru.aleshin.features.player.impl.domain.entities.PlayerFailures +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +internal interface PlayerErrorHandler : ErrorHandler { + class Base @Inject constructor() : PlayerErrorHandler { + override fun handle(throwable: Throwable) = when (throwable) { + else -> PlayerFailures.OtherError(throwable) + } + } +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/entities/PlayerFailures.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/entities/PlayerFailures.kt new file mode 100644 index 0000000..3716eee --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/entities/PlayerFailures.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.player.impl.domain.entities + +import ru.aleshin.core.common.functional.DomainFailures + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +internal sealed class PlayerFailures : DomainFailures { + data class OtherError(val throwable: Throwable) : PlayerFailures() +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/interactors/AudioInteractor.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/interactors/AudioInteractor.kt new file mode 100644 index 0000000..3b40f66 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/interactors/AudioInteractor.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.features.player.impl.domain.interactors + +import ru.aleshin.core.common.functional.DomainResult +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 ru.aleshin.features.home.api.domain.repositories.SystemAudioRepository +import ru.aleshin.features.player.impl.domain.common.PlayerEitherWrapper +import ru.aleshin.features.player.impl.domain.entities.PlayerFailures +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +internal interface AudioInteractor { + + suspend fun fetchPlaylist(type: AudioPlayListType): DomainResult + + class Base @Inject constructor( + private val systemAudioRepository: SystemAudioRepository, + private val appAudioRepository: AppAudioRepository, + private val eitherWrapper: PlayerEitherWrapper, + ) : AudioInteractor { + + override suspend fun fetchPlaylist(type: AudioPlayListType) = eitherWrapper.wrap { + return@wrap when (type) { + AudioPlayListType.SYSTEM -> systemAudioRepository.fetchPlaylist() + AudioPlayListType.APP -> appAudioRepository.fetchPlaylist() + AudioPlayListType.OTHER -> null + } + } + } +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/interactors/PlayerSettingsInteractor.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/interactors/PlayerSettingsInteractor.kt new file mode 100644 index 0000000..2bfc0d7 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/domain/interactors/PlayerSettingsInteractor.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.features.player.impl.domain.interactors + +import ru.aleshin.core.common.functional.DomainResult +import ru.aleshin.core.common.functional.UnitDomainResult +import ru.aleshin.features.player.impl.domain.common.PlayerEitherWrapper +import ru.aleshin.features.player.impl.domain.entities.PlayerFailures +import ru.aleshin.features.settings.api.domain.entities.PlayerSettings +import ru.aleshin.features.settings.api.domain.repositories.SettingsRepository +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 14.07.2023. + */ +internal interface PlayerSettingsInteractor { + + suspend fun updateSettings(settings: PlayerSettings): UnitDomainResult + suspend fun fetchSettings(): DomainResult + + class Base @Inject constructor( + private val settingsRepository: SettingsRepository, + private val eitherWrapper: PlayerEitherWrapper, + ) : PlayerSettingsInteractor { + + override suspend fun updateSettings(settings: PlayerSettings) = eitherWrapper.wrap { + val appSettings = settingsRepository.fetchSettings() + settingsRepository.updateSettings(model = appSettings.copy(player = settings)) + } + + override suspend fun fetchSettings() = eitherWrapper.wrap { + settingsRepository.fetchSettings().player + } + } +} \ No newline at end of file diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/navigation/PlayerFeatureStarterImpl.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/navigation/PlayerFeatureStarterImpl.kt new file mode 100644 index 0000000..2e375c8 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/navigation/PlayerFeatureStarterImpl.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.player.impl.navigation + +import cafe.adriel.voyager.core.screen.Screen +import ru.aleshin.features.player.api.navigation.PlayerFeatureStarter +import ru.aleshin.features.player.api.navigation.PlayerScreens +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +internal class PlayerFeatureStarterImpl @Inject constructor( + private val localNavScreen: Screen, + private val navigationManager: PlayerNavigationManager, +) : PlayerFeatureStarter { + + override fun fetchPlayerScreen(navScreen: PlayerScreens) = navigationManager.navigateToLocalScreen( + navScreen = navScreen, + isRoot = true + ).let { + return@let localNavScreen + } +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/navigation/PlayerNavigationManager.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/navigation/PlayerNavigationManager.kt new file mode 100644 index 0000000..22d37ba --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/navigation/PlayerNavigationManager.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.features.player.impl.navigation + +import cafe.adriel.voyager.core.screen.Screen +import ru.aleshin.core.common.di.FeatureRouter +import ru.aleshin.core.common.functional.MediaCommand +import ru.aleshin.core.common.functional.audio.AudioPlayListUi +import ru.aleshin.core.common.navigation.Router +import ru.aleshin.features.home.api.di.HomeScreens +import ru.aleshin.features.home.api.navigation.HomeFeatureStarter +import ru.aleshin.features.player.api.navigation.PlayerScreens +import ru.aleshin.features.player.api.presentation.common.MediaController +import ru.aleshin.features.player.impl.presentation.audio.AudioScreen +import ru.aleshin.features.player.impl.presentation.audio.screenmodel.AudioInfoCommunicator +import ru.aleshin.features.player.impl.presentation.video.VideoScreen +import ru.aleshin.features.player.impl.presentation.video.screenmodel.VideoInfoCommunicator +import javax.inject.Inject +import javax.inject.Provider + +/** + * @author Stanislav Aleshin on 04.07.2023. + */ +internal interface PlayerNavigationManager { + + suspend fun navigateToDetails(playlist: AudioPlayListUi) + fun navigateToLocalScreen(navScreen: PlayerScreens, isRoot: Boolean) + fun navigateToBack() + + class Base @Inject constructor( + @FeatureRouter private val localRouter: Router, + private val globalRouter: Router, + private val homeStarter: Provider, + private val audioInfoCommunicator: AudioInfoCommunicator, + private val videoInfoCommunicator: VideoInfoCommunicator, + private val mediaController: MediaController, + ) : PlayerNavigationManager { + + override fun navigateToLocalScreen(navScreen: PlayerScreens, isRoot: Boolean) { + when (navScreen) { + is PlayerScreens.Audio -> { + mediaController.work(MediaCommand.SelectAudio(navScreen.audio, navScreen.playListType)) + audioInfoCommunicator.update(navScreen.playListType) + localNav(AudioScreen(), isRoot) + } + is PlayerScreens.Video -> { + videoInfoCommunicator.update(navScreen.videoInfoModel) + localNav(VideoScreen(), isRoot) + } + } + } + + override suspend fun navigateToDetails(playlist: AudioPlayListUi) { + val screen = homeStarter.get().fetchHomeScreen(HomeScreens.Details(playlist)) + globalRouter.navigateTo(screen) + } + + override fun navigateToBack() = globalRouter.navigateBack() + + private fun localNav(screen: Screen, isRoot: Boolean) = with(localRouter) { + if (isRoot) replaceTo(screen, true) else navigateTo(screen) + } + } +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/AudioContent.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/AudioContent.kt new file mode 100644 index 0000000..f225a8c --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/AudioContent.kt @@ -0,0 +1,296 @@ +/* + * 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.player.impl.presentation.audio + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpSize +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.managers.toImageBitmap +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioViewState +import ru.aleshin.features.player.impl.presentation.theme.PlayerThemeRes + +/** + * @author Stanislav Aleshin on 11.07.2023. + */ +@Composable +internal fun AudioContent( + modifier: Modifier = Modifier, + state: AudioViewState, + onChangePosition: (Float) -> Unit, + onPlayPauseClick: (Boolean) -> Unit, + onNextClick: () -> Unit, + onPreviousClick: () -> Unit, +) { + val scrollState = rememberScrollState() + Column(modifier.fillMaxSize().verticalScroll(scrollState), verticalArrangement = Arrangement.SpaceEvenly) { + TrackImage( + modifier = Modifier + .padding(bottom = 48.dp) + .align(Alignment.CenterHorizontally), + image = state.currentAudio?.imagePath?.mapAudioPathToPreview(), + ) + Column { + TrackNameSection( + title = state.currentAudio?.title, + authorOrAlbum = state.currentAudio?.artist ?: state.currentAudio?.album + ) + TrackControlSection( + position = state.lostTime * (1f / (state.currentAudio?.duration ?: 1)) , + lostTime = state.lostTime.toSecondsAndMinutesString(), + nextTime = state.nextTime.toSecondsAndMinutesString(), + isPlayedMusic = state.isPlaying, + onChangePosition = onChangePosition, + onPlayPauseClick = onPlayPauseClick, + onNextClick = onNextClick, + onPreviousClick = onPreviousClick, + ) + } + } +} + +@Composable +internal fun TrackImage( + modifier: Modifier = Modifier, + image: ImageBitmap?, +) { + if (image != null) { + Image( + modifier = modifier + .size(250.dp) + .shadow(elevation = 8.dp, shape = MaterialTheme.shapes.large) + .clip(MaterialTheme.shapes.large), + bitmap = image, + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } else { + Box( + modifier = modifier + .size(250.dp) + .shadow(elevation = 8.dp, shape = MaterialTheme.shapes.large) + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(48.dp), + painter = painterResource(id = PlayerThemeRes.icons.music), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@Composable +internal fun TrackNameSection( + modifier: Modifier = Modifier, + title: String?, + authorOrAlbum: String?, +) { + Column( + modifier = modifier + .padding(vertical = 8.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier, + text = title ?: "", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold, + ), + ) +// ExoPlayer.Builder(LocalContext.current).build() + Text( + modifier = Modifier, + text = authorOrAlbum ?: "", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + ) + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +internal fun TrackControlSection( + modifier: Modifier = Modifier, + position: Float, + lostTime: String, + nextTime: String, + isPlayedMusic: Boolean, + onChangePosition: (Float) -> Unit, + onPlayPauseClick: (Boolean) -> Unit, + onNextClick: () -> Unit, + onPreviousClick: () -> Unit, +) { + Column( + modifier = modifier.padding(top = 16.dp, bottom = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(36.dp), + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + val interactionSource = remember { MutableInteractionSource() } + Slider( + value = position, + onValueChange = onChangePosition, + interactionSource = interactionSource, + thumb = { + SliderDefaults.Thumb( + interactionSource = interactionSource, + thumbSize = DpSize(width = 15.dp, height = 15.dp), + ) + } + ) + Row( + modifier = Modifier + .padding(horizontal = 8.dp) + .offset(y = (-8).dp) + ) { + Text( + text = lostTime, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = nextTime, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + ) + } + } + Row( + modifier = Modifier.padding(bottom = 36 .dp), + horizontalArrangement = Arrangement.spacedBy(42.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + modifier = Modifier.size(56.dp), + onClick = onPreviousClick + ) { + Icon( + modifier = Modifier.size(36.dp), + painter = painterResource(id = PlayerThemeRes.icons.previous), + contentDescription = PlayerThemeRes.strings.previousTrackDesk, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + IconButton( + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(100.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + onClick = { onPlayPauseClick(!isPlayedMusic) } + ) { + Icon( + modifier = Modifier.size(36.dp), + painter = when (isPlayedMusic) { + true -> painterResource(id = PlayerThemeRes.icons.pause) + false -> painterResource(id = PlayerThemeRes.icons.play) + }, + contentDescription = PlayerThemeRes.strings.previousTrackDesk, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + IconButton( + modifier = Modifier.size(56.dp), + onClick = onNextClick + ) { + Icon( + modifier = Modifier.size(36.dp), + painter = painterResource(id = PlayerThemeRes.icons.next), + contentDescription = PlayerThemeRes.strings.previousTrackDesk, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } + } +} + +//@Composable +//@Preview +//internal fun AudioContent_PreviewLight() { +// MixPlayerTheme(themeType = ThemeUiType.LIGHT) { +// PlayerTheme { +// Box(Modifier.background(MaterialTheme.colorScheme.background)) { +// AudioContent( +// state = AudioViewState(), +// onChangePosition = {}, +// onPlayPauseClick = {}, +// onNextClick = {}, +// onPreviousClick = {}, +// ) +// } +// } +// } +//} +// +//@Composable +//@Preview +//internal fun AudioContent_PreviewDark() { +// MixPlayerTheme(themeType = ThemeUiType.DARK) { +// PlayerTheme { +// Box { +// AudioContent( +// state = AudioViewState(), +// onChangePosition = {}, +// onPlayPauseClick = {}, +// onNextClick = {}, +// onPreviousClick = {}, +// ) +// } +// } +// } +//} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/AudioScreen.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/AudioScreen.kt new file mode 100644 index 0000000..5fdacd1 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/AudioScreen.kt @@ -0,0 +1,116 @@ +/* + * 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.player.impl.presentation.audio + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.remember +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.media3.common.MediaItem +import androidx.media3.exoplayer.ExoPlayer +import cafe.adriel.voyager.core.screen.Screen +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import ru.aleshin.core.common.platform.screen.ScreenContent +import ru.aleshin.core.ui.theme.MixPlayerRes +import ru.aleshin.core.ui.views.ErrorSnackbar +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioAction +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioEffect +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioEvent +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioViewState +import ru.aleshin.features.player.impl.presentation.mappers.mapToMessage +import ru.aleshin.features.player.impl.presentation.theme.PlayerTheme +import ru.aleshin.features.player.impl.presentation.theme.PlayerThemeRes +import ru.aleshin.features.player.impl.presentation.audio.screenmodel.rememberAudioScreenModel +import ru.aleshin.features.player.impl.presentation.audio.views.AudioTopBar +import ru.aleshin.features.player.impl.presentation.mappers.mapToString +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 11.07.2023 + */ +internal class AudioScreen @Inject constructor() : Screen { + + @Composable + override fun Content() = ScreenContent( + screenModel = rememberAudioScreenModel(), + initialState = AudioViewState(), + ) { state -> + PlayerTheme { + val strings = PlayerThemeRes.strings + val snackbarState = remember { SnackbarHostState() } + + Scaffold( + modifier = Modifier.fillMaxSize(), + content = { paddingValues -> + AudioContent( + state = state, + modifier = Modifier.padding(paddingValues), + onChangePosition = { dispatchEvent(AudioEvent.UpdatePosition(it)) }, + onPlayPauseClick = { dispatchEvent(AudioEvent.PressPlayOrPauseButton) }, + onNextClick = { dispatchEvent(AudioEvent.PressNextButton) }, + onPreviousClick = { dispatchEvent(AudioEvent.PressPreviousButton) }, + ) + }, + topBar = { + AudioTopBar( + onBackPress = { dispatchEvent(AudioEvent.PressBackButton) }, + playlistName = state.playList?.listType?.mapToString(), + volume = state.volume, + onVolumeChange = { dispatchEvent(AudioEvent.ChangeVolume(it)) } + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarState) { ErrorSnackbar(it) } + }, + ) + + handleEffect { effect -> + when (effect) { + is AudioEffect.ShowError -> { + snackbarState.showSnackbar( + message = effect.failures.mapToMessage(strings), + withDismissAction = true, + ) + } + AudioEffect.NextTrack -> { dispatchEvent(AudioEvent.PressNextButton) } + } + } + AudioSystemUi() + } + } + + @Composable + private fun AudioSystemUi() { + val systemUiController = rememberSystemUiController() + val navBarColor = MaterialTheme.colorScheme.background + val statusBarColor = MaterialTheme.colorScheme.background + val isDarkIcons = MixPlayerRes.colorsType.isDark + + SideEffect { + systemUiController.setNavigationBarColor(color = navBarColor, darkIcons = !isDarkIcons) + systemUiController.setStatusBarColor(color = statusBarColor, darkIcons = !isDarkIcons) + } + } +} \ No newline at end of file diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/contract/AudioContract.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/contract/AudioContract.kt new file mode 100644 index 0000000..74197d4 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/contract/AudioContract.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.features.player.impl.presentation.audio.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.functional.audio.PlayerInfo +import ru.aleshin.core.common.platform.screenmodel.contract.* +import ru.aleshin.features.home.api.domain.entities.AudioPlayList +import ru.aleshin.features.player.impl.domain.entities.PlayerFailures + +/** + * @author Stanislav Aleshin on 11.07.2023 + */ +@Parcelize +internal data class AudioViewState( + val isPlaying: Boolean = false, + val currentAudio: AudioInfoUi? = null, + val playList: AudioPlayListUi? = null, + val lostTime: Long = 0L, + val nextTime: Long = 0L, + val volume: Float = 1f, +) : BaseViewState + +internal sealed class AudioEvent : BaseEvent { + object Init : AudioEvent() + object PressBackButton : AudioEvent() + object PressNextButton : AudioEvent() + object PressPreviousButton : AudioEvent() + object PressPlayOrPauseButton : AudioEvent() + data class ChangeVolume(val value: Float) : AudioEvent() + data class UpdatePosition(val value: Float) : AudioEvent() +} + +internal sealed class AudioEffect : BaseUiEffect { + data class ShowError(val failures: PlayerFailures) : AudioEffect() + object NextTrack : AudioEffect() +} + +internal sealed class AudioAction : BaseAction { + data class UpdateVolume(val value: Float) : AudioAction() + data class UpdatePlayerInfo(val playerInfo: PlayerInfo) : AudioAction() + data class UpdatePlayList(val playlist: AudioPlayListUi?) : AudioAction() + object Navigate : AudioAction() +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioEffectCommunicator.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioEffectCommunicator.kt new file mode 100644 index 0000000..79c29f7 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioEffectCommunicator.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.player.impl.presentation.audio.screenmodel + +import ru.aleshin.core.common.platform.communications.state.EffectCommunicator +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioEffect +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 11.07.2023. + */ +internal interface AudioEffectCommunicator : EffectCommunicator { + + class Base @Inject constructor() : AudioEffectCommunicator, + EffectCommunicator.Abstract() +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioInfoCommunicator.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioInfoCommunicator.kt new file mode 100644 index 0000000..c87f66f --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioInfoCommunicator.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.features.player.impl.presentation.audio.screenmodel + +import ru.aleshin.core.common.functional.audio.AudioPlayListType +import ru.aleshin.core.common.platform.communications.Communicator +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.07.2023. + */ +internal interface AudioInfoCommunicator : Communicator { + class Base @Inject constructor() : AudioInfoCommunicator, + Communicator.AbstractStateFlow(null) +} \ No newline at end of file diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioScreenModel.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioScreenModel.kt new file mode 100644 index 0000000..c3fd294 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioScreenModel.kt @@ -0,0 +1,141 @@ +/* + * 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.player.impl.presentation.audio.screenmodel + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import ru.aleshin.core.common.functional.MediaCommand +import ru.aleshin.core.common.managers.CoroutineManager +import ru.aleshin.core.common.platform.screenmodel.BaseScreenModel +import ru.aleshin.core.common.platform.screenmodel.work.WorkScope +import ru.aleshin.features.player.impl.navigation.PlayerNavigationManager +import ru.aleshin.features.player.impl.di.holder.PlayerComponentHolder +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioAction +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioEffect +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioEvent +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioViewState +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 11.07.2023 + */ +internal class AudioScreenModel @Inject constructor( + private val navigationManager: PlayerNavigationManager, + private val audioWorkProcessor: AudioWorkProcessor, + stateCommunicator: AudioStateCommunicator, + effectCommunicator: AudioEffectCommunicator, + coroutineManager: CoroutineManager, +) : BaseScreenModel( + stateCommunicator = stateCommunicator, + effectCommunicator = effectCommunicator, + coroutineManager = coroutineManager, +) { + + override fun init() { + if (!isInitialize.get()) { + super.init() + dispatchEvent(AudioEvent.Init) + } + } + + override suspend fun WorkScope.handleEvent( + event: AudioEvent, + ) { + when (event) { + AudioEvent.Init -> { + launchBackgroundWork(AudioWorkCommand.LoadPlayList) { + audioWorkProcessor.work(AudioWorkCommand.LoadPlayList).collectAndHandleWork() + } + launchBackgroundWork(AudioWorkCommand.ReceivePlayerInfo) { + audioWorkProcessor.work(AudioWorkCommand.ReceivePlayerInfo).collectAndHandleWork() + } + audioWorkProcessor.work(AudioWorkCommand.SetUpParameters).collectAndHandleWork() + } + is AudioEvent.ChangeVolume -> { + AudioWorkCommand.SaveVolume(state().volume).apply { + audioWorkProcessor.work(this).collectAndHandleWork() + } + AudioWorkCommand.SendMediaCommand(MediaCommand.ChangeVolume(event.value)).apply { + audioWorkProcessor.work(this).collectAndHandleWork() + } + } + is AudioEvent.PressNextButton -> { + val playList = state().playList + if (playList != null) { + val currentAudio = state().currentAudio + val audioIndex = playList.audioList.indexOfFirst { it.id == currentAudio?.id } + val audio = playList.audioList.getOrNull(audioIndex + 1) ?: return + val command = AudioWorkCommand.SendMediaCommand(MediaCommand.SelectAudio(audio, playList.listType)) + audioWorkProcessor.work(command).collectAndHandleWork() + } + } + is AudioEvent.PressPreviousButton -> { + val playList = state().playList + if (playList != null) { + val currentAudio = state().currentAudio + val audioIndex = playList.audioList.indexOfFirst { it.id == currentAudio?.id } + val audio = playList.audioList.getOrNull(audioIndex - 1) ?: return + val command = AudioWorkCommand.SendMediaCommand(MediaCommand.SelectAudio(audio, playList.listType)) + audioWorkProcessor.work(command).collectAndHandleWork() + } + } + is AudioEvent.PressPlayOrPauseButton -> { + val command = AudioWorkCommand.SendMediaCommand(MediaCommand.PlayOrPause) + audioWorkProcessor.work(command).collectAndHandleWork() + } + is AudioEvent.UpdatePosition -> { + val position = event.value * checkNotNull(state().currentAudio?.duration) + val command = AudioWorkCommand.SendMediaCommand(MediaCommand.SeekTo(position.toInt())) + audioWorkProcessor.work(command).collectAndHandleWork() + } + is AudioEvent.PressBackButton -> navigationManager.navigateToBack() + } + } + + override suspend fun reduce( + action: AudioAction, + currentState: AudioViewState, + ) = when (action) { + is AudioAction.Navigate -> currentState + is AudioAction.UpdatePlayerInfo -> currentState.copy( + currentAudio = action.playerInfo.playback.currentAudio, + isPlaying = action.playerInfo.playback.isPlay, + lostTime = action.playerInfo.playback.position.toLong(), + nextTime = action.playerInfo.playback.currentAudio?.duration?.minus(action.playerInfo.playback.position) ?: 0L, + volume = action.playerInfo.playback.volume, + ) + is AudioAction.UpdatePlayList -> currentState.copy( + playList = action.playlist, + ) + is AudioAction.UpdateVolume -> currentState.copy( + volume = action.value, + ) + + } + + override fun onDispose() { + super.onDispose() + PlayerComponentHolder.clear() + } +} + +@Composable +internal fun Screen.rememberAudioScreenModel(): AudioScreenModel { + val component = PlayerComponentHolder.fetchComponent() + return rememberScreenModel { component.fetchAudioScreenModel() } +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioStateCommunicator.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioStateCommunicator.kt new file mode 100644 index 0000000..67ea1a0 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioStateCommunicator.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.player.impl.presentation.audio.screenmodel + +import ru.aleshin.core.common.platform.communications.state.StateCommunicator +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioViewState +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 11.07.2023. + */ +internal interface AudioStateCommunicator : StateCommunicator { + + class Base @Inject constructor() : AudioStateCommunicator, + StateCommunicator.Abstract(defaultState = AudioViewState()) +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioWorkProcessor.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioWorkProcessor.kt new file mode 100644 index 0000000..556557a --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/screenmodel/AudioWorkProcessor.kt @@ -0,0 +1,111 @@ +/* + * 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.player.impl.presentation.audio.screenmodel + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import ru.aleshin.core.common.functional.Either +import ru.aleshin.core.common.functional.MediaCommand +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.features.player.impl.domain.interactors.AudioInteractor +import ru.aleshin.features.player.impl.domain.interactors.PlayerSettingsInteractor +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioAction +import ru.aleshin.features.player.impl.presentation.audio.contract.AudioEffect +import ru.aleshin.features.player.impl.presentation.mappers.mapToUi +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 11.07.2023. + */ +internal interface AudioWorkProcessor : FlowWorkProcessor { + + class Base @Inject constructor( + private val playerSettingsInteractor: PlayerSettingsInteractor, + private val audioInteractor: AudioInteractor, + private val audioInfoCommunicator: AudioInfoCommunicator, + private val mediaController: MediaController, + private val playbackManager: PlaybackManager, + ) : AudioWorkProcessor { + + override suspend fun work(command: AudioWorkCommand) = when (command) { + is AudioWorkCommand.SendMediaCommand -> sendMediaCommandWork(command.command) + is AudioWorkCommand.SaveVolume -> saveVolumeWork(command.volume) + is AudioWorkCommand.SetUpParameters -> setUpParametersWork() + is AudioWorkCommand.LoadPlayList -> loadPlayListWork() + is AudioWorkCommand.ReceivePlayerInfo -> receivePlayerInfoWork() + } + + private fun receivePlayerInfoWork() = flow { + playbackManager.collectInfo { playerInfo -> + emit(ActionResult(AudioAction.UpdatePlayerInfo(playerInfo))) + if (playerInfo.playback.isComplete) emit(EffectResult(AudioEffect.NextTrack)) + } + } + + private fun loadPlayListWork() = flow { + audioInfoCommunicator.collect { type -> + if (type != null) { + when (val result = audioInteractor.fetchPlaylist(type)) { + is Either.Right -> emit(ActionResult(AudioAction.UpdatePlayList(result.data?.mapToUi()))) + is Either.Left -> emit(EffectResult(AudioEffect.ShowError(result.data))) + } + } + } + } + + private fun setUpParametersWork() = flow { + when (val result = playerSettingsInteractor.fetchSettings()) { + is Either.Right -> emit(ActionResult(AudioAction.UpdateVolume(result.data.volume))).apply { + delay(200L) + mediaController.work(MediaCommand.ChangeVolume(result.data.volume)) + } + is Either.Left -> emit(EffectResult(AudioEffect.ShowError(result.data))) + } + } + + private fun saveVolumeWork(volume: Float) = flow { + val settings = playerSettingsInteractor.fetchSettings().let { + if (it is Either.Right) it.data + else return@flow emit(EffectResult(AudioEffect.ShowError((it as Either.Left).data))) + } + when (val result = playerSettingsInteractor.updateSettings(settings.copy(volume = volume))) { + is Either.Right -> Unit + is Either.Left -> emit(EffectResult(AudioEffect.ShowError(result.data))) + } + } + + private fun sendMediaCommandWork( + command: MediaCommand + ) = flow> { + mediaController.work(command) + } + } +} + +internal sealed class AudioWorkCommand : WorkCommand { + object LoadPlayList : AudioWorkCommand() + object ReceivePlayerInfo : AudioWorkCommand() + object SetUpParameters : AudioWorkCommand() + data class SaveVolume(val volume: Float) : AudioWorkCommand() + data class SendMediaCommand(val command: MediaCommand) : AudioWorkCommand() +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/views/AudioTopBar.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/views/AudioTopBar.kt new file mode 100644 index 0000000..9483243 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/audio/views/AudioTopBar.kt @@ -0,0 +1,107 @@ +/* + * 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.player.impl.presentation.audio.views + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +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.res.painterResource +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import ru.aleshin.core.ui.views.TopAppBarAction +import ru.aleshin.core.ui.views.TopAppBarEmptyButton +import ru.aleshin.core.ui.views.TopAppBarMoreActions +import ru.aleshin.core.ui.views.TopAppBarTitle +import ru.aleshin.features.player.impl.R +import ru.aleshin.features.player.impl.presentation.theme.PlayerThemeRes + +/** + * @author Stanislav Aleshin on 13.07.2023. + */ +@Composable +@OptIn(ExperimentalMaterial3Api::class) +internal fun AudioTopBar( + modifier: Modifier = Modifier, + onBackPress: () -> Unit, + playlistName: String?, + volume: Float, + onVolumeChange: (Float) -> Unit, +) { + TopAppBar( + modifier = modifier, + title = { + TopAppBarTitle(text = playlistName ?: "") + }, + navigationIcon = { + IconButton(onClick = onBackPress) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = PlayerThemeRes.strings.backDesk, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + }, + actions = { + val expanded = rememberSaveable { mutableStateOf(false) } + Box(modifier = modifier.wrapContentSize(Alignment.TopEnd)) { + IconButton(onClick = { expanded.value = true }) { + Icon( + painter = painterResource(id = PlayerThemeRes.icons.volume), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + DropdownMenu( + modifier = Modifier.align(Alignment.TopCenter).background(MaterialTheme.colorScheme.surfaceContainerHigh), + expanded = expanded.value, + offset = DpOffset(0.dp, 10.dp), + onDismissRequest = { expanded.value = false }, + ) { + Slider( + modifier = Modifier.width(200.dp), + value = volume, + onValueChange = onVolumeChange + ) + } + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + ), + ) +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/mappers/AudioUiMappers.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/mappers/AudioUiMappers.kt new file mode 100644 index 0000000..6d1525b --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/mappers/AudioUiMappers.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.player.impl.presentation.mappers + +import ru.aleshin.core.common.functional.audio.AudioInfoUi +import ru.aleshin.core.common.functional.audio.AudioPlayListUi +import ru.aleshin.core.common.managers.BitmapUtils +import ru.aleshin.features.home.api.domain.entities.AudioInfo +import ru.aleshin.features.home.api.domain.entities.AudioPlayList + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +internal fun AudioInfo.mapToUi() = AudioInfoUi( + id = id, + path = path, + title = title, + artist = artist, + album = album, + imagePath = imagePath, + duration = durationInMillis, + date = date, +) + +internal fun AudioPlayList.mapToUi() = AudioPlayListUi( + listType = type, + audioList = audioList.map { it.mapToUi() } +) + +internal fun AudioInfoUi.mapToDomain() = AudioInfo( + id = id, + path = path, + title = title, + artist = artist, + album = album, + imagePath = imagePath, + durationInMillis = duration, + date = date, +) + +internal fun AudioPlayListUi.mapToDomain() = AudioPlayList( + type = listType, + audioList = audioList.map { it.mapToDomain() } +) diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/mappers/AuthFailureMapper.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/mappers/AuthFailureMapper.kt new file mode 100644 index 0000000..aa1e3c6 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/mappers/AuthFailureMapper.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.player.impl.presentation.mappers + +import ru.aleshin.features.player.impl.domain.entities.PlayerFailures +import ru.aleshin.features.player.impl.presentation.theme.tokens.PlayerStrings + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +internal fun PlayerFailures.mapToMessage(strings: PlayerStrings) = when (this) { + is PlayerFailures.OtherError -> strings.otherError +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/mappers/PlaylistTypeMapper.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/mappers/PlaylistTypeMapper.kt new file mode 100644 index 0000000..09ac6ac --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/mappers/PlaylistTypeMapper.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.player.impl.presentation.mappers + +import androidx.compose.runtime.Composable +import ru.aleshin.core.common.functional.audio.AudioPlayListType +import ru.aleshin.features.player.impl.presentation.theme.PlayerThemeRes + +/** + * @author Stanislav Aleshin on 13.07.2023. + */ +@Composable +internal fun AudioPlayListType.mapToString() = when (this) { + AudioPlayListType.SYSTEM -> PlayerThemeRes.strings.systemAudioTitle + AudioPlayListType.APP -> PlayerThemeRes.strings.appAudioTitle + AudioPlayListType.OTHER -> name +} \ No newline at end of file diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/nav/NavScreen.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/nav/NavScreen.kt new file mode 100644 index 0000000..d0c067c --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/nav/NavScreen.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.player.impl.presentation.nav + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.CurrentScreen +import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior +import ru.aleshin.core.common.navigation.navigator.AppNavigator +import ru.aleshin.core.common.navigation.navigator.rememberNavigatorManager +import ru.aleshin.features.player.impl.presentation.theme.PlayerTheme +import ru.aleshin.features.player.impl.di.holder.PlayerComponentHolder +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +internal class NavScreen @Inject constructor() : Screen { + + @Composable + override fun Content() = PlayerTheme { + AppNavigator( + navigatorManager = rememberNavigatorManager { + PlayerComponentHolder.fetchComponent().fetchLocalNavigatorManager() + }, + content = { CurrentScreen() }, + ) + } +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/PlayerTheme.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/PlayerTheme.kt new file mode 100644 index 0000000..9b8d600 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/PlayerTheme.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.player.impl.presentation.theme + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import ru.aleshin.core.ui.theme.MixPlayerRes +import ru.aleshin.core.ui.theme.tokens.MixPlayerLanguage +import ru.aleshin.features.player.impl.presentation.theme.tokens.LocalPlayerIcons +import ru.aleshin.features.player.impl.presentation.theme.tokens.LocalPlayerStrings +import ru.aleshin.features.player.impl.presentation.theme.tokens.PlayerIcons +import ru.aleshin.features.player.impl.presentation.theme.tokens.PlayerStrings + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +@Composable +internal fun PlayerTheme(content: @Composable () -> Unit) { + val icons = PlayerIcons.DEFAULT + val strings = when (MixPlayerRes.language) { + MixPlayerLanguage.EN -> PlayerStrings.ENGLISH + MixPlayerLanguage.RU -> PlayerStrings.RUSSIAN + } + + CompositionLocalProvider( + LocalPlayerIcons provides icons, + LocalPlayerStrings provides strings, + content = content, + ) +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/PlayerThemeRes.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/PlayerThemeRes.kt new file mode 100644 index 0000000..6625308 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/PlayerThemeRes.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.player.impl.presentation.theme + +import androidx.compose.runtime.Composable +import ru.aleshin.features.player.impl.presentation.theme.tokens.PlayerIcons +import ru.aleshin.features.player.impl.presentation.theme.tokens.PlayerStrings +import ru.aleshin.features.player.impl.presentation.theme.tokens.LocalPlayerIcons +import ru.aleshin.features.player.impl.presentation.theme.tokens.LocalPlayerStrings + +/** + * @author Stanislav Aleshin on 14.06.2023 + */ +internal object PlayerThemeRes { + + val icons: PlayerIcons + @Composable get() = LocalPlayerIcons.current + + val strings: PlayerStrings + @Composable get() = LocalPlayerStrings.current +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/tokens/PlayerIcons.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/tokens/PlayerIcons.kt new file mode 100644 index 0000000..ceb7642 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/tokens/PlayerIcons.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.features.player.impl.presentation.theme.tokens + +import androidx.compose.runtime.staticCompositionLocalOf +import ru.aleshin.features.player.impl.R + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +internal data class PlayerIcons( + val next: Int, + val previous: Int, + val pause: Int, + val play: Int, + val music: Int, + val volume: Int, + val orientation: Int, +) { + companion object { + val DEFAULT = PlayerIcons( + next = R.drawable.ic_next, + previous = R.drawable.ic_previous, + pause = R.drawable.ic_pause, + play = R.drawable.ic_play, + music = R.drawable.ic_music, + volume = R.drawable.ic_volume, + orientation = R.drawable.ic_orientation, + ) + } +} + +internal val LocalPlayerIcons = staticCompositionLocalOf { + error("Player Icons is not provided") +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/tokens/PlayerStrings.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/tokens/PlayerStrings.kt new file mode 100644 index 0000000..a45515e --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/theme/tokens/PlayerStrings.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 + * imitations under the License. + */ + +package ru.aleshin.features.player.impl.presentation.theme.tokens + +import androidx.compose.runtime.staticCompositionLocalOf + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +internal data class PlayerStrings( + val playerHeader: String, + val otherError: String, + val backDesk: String?, + val systemAudioTitle: String, + val appAudioTitle: String, + val previousTrackDesk: String, + val nextTrackDesk: String, + val playPauseTrackDesk: String, + val orientationDesk: String, +) { + companion object { + val RUSSIAN = PlayerStrings( + playerHeader = "Проигрыватель", + otherError = "Ошибка! Обратитесь к разработчику!", + backDesk = "Назад", + systemAudioTitle = "Системные треки", + appAudioTitle = "Треки приложения", + previousTrackDesk = "Предыдущий трек", + nextTrackDesk = "Следующий трек", + playPauseTrackDesk = "Плей/Пауза", + orientationDesk = "Сменить ориентацию", + ) + val ENGLISH = PlayerStrings( + playerHeader = "Player", + otherError = "Error! Contact the developer!", + backDesk = "Back", + systemAudioTitle = "System tracks", + appAudioTitle = "App tracks", + previousTrackDesk = "Previous track", + nextTrackDesk = "Next track", + playPauseTrackDesk = "Play/Pause", + orientationDesk = "Change orientation" + ) + } +} + +internal val LocalPlayerStrings = staticCompositionLocalOf { + error("Player Strings is not provided") +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/VideoContent.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/VideoContent.kt new file mode 100644 index 0000000..dcc7572 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/VideoContent.kt @@ -0,0 +1,124 @@ +/* + * 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.player.impl.presentation.video + +import android.util.Log +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +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.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.MediaItem +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView +import ru.aleshin.core.common.functional.video.VideoInfoUi +import ru.aleshin.features.player.impl.presentation.video.contract.VideoViewState + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +@Composable +internal fun VideoContent( + state: VideoViewState, + modifier: Modifier = Modifier, +) { + val mContext = LocalContext.current + var video by rememberSaveable { mutableStateOf(null) } + var position by rememberSaveable { mutableLongStateOf(0L) } + val mExoPlayer = remember(mContext) { + ExoPlayer.Builder(mContext).build().apply { seekTo(position); playWhenReady = true } + } + + LaunchedEffect(state.videoInfo) { + if (state.videoInfo != null) { + if (state.videoInfo != video) { + mExoPlayer.apply { + clearMediaItems() + addMediaItem(MediaItem.fromUri(state.videoInfo.path)) + seekTo(0) + prepare() + } + video = state.videoInfo + position = 0 + } else { + mExoPlayer.apply { + addMediaItem(MediaItem.fromUri(state.videoInfo.path)) + prepare() + } + } + } + } + + DisposableEffect( + key1 = AndroidView( + modifier = modifier.fillMaxSize(), + factory = { context -> + PlayerView(context).apply { + player = mExoPlayer + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + }, + ) + ) { + onDispose { + position = mExoPlayer.currentPosition + mExoPlayer.release() + } + } +} + +//@Composable +//@Preview +//internal fun VideoContent_PreviewLight() { +// MixPlayerTheme(themeType = ThemeUiType.LIGHT) { +// PlayerTheme { +// VideoContent( +// state = VideoViewState(), +// ) +// } +// } +//} +// +//@Composable +//@Preview +//internal fun VideoContent_PreviewDark() { +// MixPlayerTheme(themeType = ThemeUiType.DARK) { +// PlayerTheme { +// VideoContent( +// state = VideoViewState(), +// ) +// } +// } +//} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/VideoScreen.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/VideoScreen.kt new file mode 100644 index 0000000..9a1af1c --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/VideoScreen.kt @@ -0,0 +1,121 @@ +/* + * 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.player.impl.presentation.video + +import android.app.Activity +import android.content.pm.ActivityInfo +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import cafe.adriel.voyager.core.screen.Screen +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import ru.aleshin.core.common.platform.screen.ScreenContent +import ru.aleshin.core.ui.theme.MixPlayerRes +import ru.aleshin.core.ui.views.ErrorSnackbar +import ru.aleshin.features.player.impl.presentation.mappers.mapToMessage +import ru.aleshin.features.player.impl.presentation.theme.PlayerThemeRes +import ru.aleshin.features.player.impl.presentation.theme.PlayerTheme +import ru.aleshin.features.player.impl.presentation.video.contract.VideoEffect +import ru.aleshin.features.player.impl.presentation.video.contract.VideoEvent +import ru.aleshin.features.player.impl.presentation.video.contract.VideoViewState +import ru.aleshin.features.player.impl.presentation.video.screenmodel.rememberVideoScreenModel +import ru.aleshin.features.player.impl.presentation.video.views.VideoTopBar +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023 + */ +internal class VideoScreen @Inject constructor() : Screen { + + @Composable + override fun Content() = ScreenContent( + screenModel = rememberVideoScreenModel(), + initialState = VideoViewState(), + ) { state -> + PlayerTheme { + val context = LocalContext.current + val strings = PlayerThemeRes.strings + val snackbarState = remember { SnackbarHostState() } + + Scaffold( + modifier = Modifier.fillMaxSize(), + content = { paddingValues -> + VideoContent( + state = state, + modifier = Modifier + .padding(paddingValues) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + ) + }, + topBar = { + VideoTopBar( + videoName = state.videoInfo?.title, + onBackPress = { dispatchEvent(VideoEvent.PressBackButton) }, + onOrientationChanged = { dispatchEvent(VideoEvent.PressOrientationButton) } + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarState) { + ErrorSnackbar(snackbarData = it) + } + }, + ) + + handleEffect { effect -> + when (effect) { + is VideoEffect.ShowError -> { + snackbarState.showSnackbar( + message = effect.failures.mapToMessage(strings), + withDismissAction = true, + ) + } + } + } + + LaunchedEffect(key1 = state.isPortraitOrientation) { + val activity = context as Activity + if (state.isPortraitOrientation) { + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } else { + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } + } + } + VideoSystemUi() + } + + @Composable + private fun VideoSystemUi() { + val systemUiController = rememberSystemUiController() + val systemsBarColor = MaterialTheme.colorScheme.surfaceContainerHigh + val isDarkIcons = MixPlayerRes.colorsType.isDark + + SideEffect { + systemUiController.setSystemBarsColor(color = systemsBarColor, darkIcons = !isDarkIcons) + } + } +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/contract/VideoContract.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/contract/VideoContract.kt new file mode 100644 index 0000000..2814dbb --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/contract/VideoContract.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.player.impl.presentation.video.contract + +import kotlinx.parcelize.Parcelize +import ru.aleshin.core.common.functional.video.VideoInfoUi +import ru.aleshin.core.common.platform.screenmodel.contract.* +import ru.aleshin.features.player.impl.domain.entities.PlayerFailures + +/** + * @author Stanislav Aleshin on 15.06.2023 + */ +@Parcelize +internal data class VideoViewState( + val videoInfo: VideoInfoUi? = null, + val isPortraitOrientation: Boolean = true, +) : BaseViewState + +internal sealed class VideoEvent : BaseEvent { + object Init : VideoEvent() + object PressBackButton : VideoEvent() + object PressOrientationButton : VideoEvent() +} + +internal sealed class VideoEffect : BaseUiEffect { + data class ShowError(val failures: PlayerFailures) : VideoEffect() +} + +internal sealed class VideoAction : BaseAction { + object Navigate : VideoAction() + data class UpdateVideo(val videoInfo: VideoInfoUi?) : VideoAction() + data class UpdateOrientation(val isPortrait: Boolean) : VideoAction() +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoEffectCommunicator.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoEffectCommunicator.kt new file mode 100644 index 0000000..1336956 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoEffectCommunicator.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.player.impl.presentation.video.screenmodel + +import ru.aleshin.core.common.platform.communications.state.EffectCommunicator +import ru.aleshin.features.player.impl.presentation.video.contract.VideoEffect +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +internal interface VideoEffectCommunicator : EffectCommunicator { + + class Base @Inject constructor() : VideoEffectCommunicator, + EffectCommunicator.Abstract() +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoInfoCommunicator.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoInfoCommunicator.kt new file mode 100644 index 0000000..8c54423 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoInfoCommunicator.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.player.impl.presentation.video.screenmodel + +import ru.aleshin.core.common.functional.video.VideoInfoUi +import ru.aleshin.core.common.platform.communications.Communicator +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.07.2023. + */ +internal interface VideoInfoCommunicator : Communicator { + class Base @Inject constructor() : VideoInfoCommunicator, Communicator.AbstractStateFlow(null) +} \ No newline at end of file diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoScreenModel.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoScreenModel.kt new file mode 100644 index 0000000..73e3c54 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoScreenModel.kt @@ -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. + */ + +package ru.aleshin.features.player.impl.presentation.video.screenmodel + +import android.util.Log +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import ru.aleshin.core.common.managers.CoroutineManager +import ru.aleshin.core.common.platform.screenmodel.BaseScreenModel +import ru.aleshin.core.common.platform.screenmodel.work.WorkScope +import ru.aleshin.features.player.impl.navigation.PlayerNavigationManager +import ru.aleshin.features.player.impl.di.holder.PlayerComponentHolder +import ru.aleshin.features.player.impl.presentation.video.contract.VideoAction +import ru.aleshin.features.player.impl.presentation.video.contract.VideoEffect +import ru.aleshin.features.player.impl.presentation.video.contract.VideoEvent +import ru.aleshin.features.player.impl.presentation.video.contract.VideoViewState +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023 + */ +internal class VideoScreenModel @Inject constructor( + private val navigationManager: PlayerNavigationManager, + private val videoWorkProcessor: VideoWorkProcessor, + stateCommunicator: VideoStateCommunicator, + effectCommunicator: VideoEffectCommunicator, + coroutineManager: CoroutineManager, +) : BaseScreenModel( + stateCommunicator = stateCommunicator, + effectCommunicator = effectCommunicator, + coroutineManager = coroutineManager, +) { + + override fun init() { + if (!isInitialize.get()) { + super.init() + dispatchEvent(VideoEvent.Init) + } + } + + override suspend fun WorkScope.handleEvent( + event: VideoEvent, + ) { + when (event) { + is VideoEvent.Init -> launchBackgroundWork(VideoWorkCommand.LoadVideo) { + videoWorkProcessor.work(VideoWorkCommand.LoadVideo).collectAndHandleWork() + } + is VideoEvent.PressOrientationButton -> { + sendAction(VideoAction.UpdateOrientation(!state().isPortraitOrientation)) + } + is VideoEvent.PressBackButton -> navigationManager.navigateToBack() + } + } + + override suspend fun reduce( + action: VideoAction, + currentState: VideoViewState, + ) = when (action) { + is VideoAction.Navigate -> currentState.copy() + is VideoAction.UpdateVideo -> currentState.copy( + videoInfo = action.videoInfo, + ) + is VideoAction.UpdateOrientation -> currentState.copy( + isPortraitOrientation = action.isPortrait, + ) + } + + override fun onDispose() { + Log.d("test", "dispose") + PlayerComponentHolder.clear() + super.onDispose() + } +} + +@Composable +internal fun Screen.rememberVideoScreenModel(): VideoScreenModel { + val component = PlayerComponentHolder.fetchComponent() + return rememberScreenModel { component.fetchVideoScreenModel() } +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoStateCommunicator.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoStateCommunicator.kt new file mode 100644 index 0000000..ee35f5c --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoStateCommunicator.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.player.impl.presentation.video.screenmodel + +import ru.aleshin.core.common.platform.communications.state.StateCommunicator +import ru.aleshin.features.player.impl.presentation.video.contract.VideoViewState +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +internal interface VideoStateCommunicator : StateCommunicator { + + class Base @Inject constructor() : VideoStateCommunicator, + StateCommunicator.Abstract(defaultState = VideoViewState()) +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoWorkProcessor.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoWorkProcessor.kt new file mode 100644 index 0000000..67bc3a3 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/screenmodel/VideoWorkProcessor.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 + * imitations under the License. + */ + +package ru.aleshin.features.player.impl.presentation.video.screenmodel + +import android.util.Log +import kotlinx.coroutines.flow.flow +import ru.aleshin.core.common.platform.screenmodel.work.ActionResult +import ru.aleshin.core.common.platform.screenmodel.work.FlowWorkProcessor +import ru.aleshin.core.common.platform.screenmodel.work.WorkCommand +import ru.aleshin.features.player.impl.navigation.PlayerNavigationManager +import ru.aleshin.features.player.impl.presentation.video.contract.VideoAction +import ru.aleshin.features.player.impl.presentation.video.contract.VideoEffect +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 05.07.2023. + */ +internal interface VideoWorkProcessor : FlowWorkProcessor { + + class Base @Inject constructor( + private val videoInfoCommunicator: VideoInfoCommunicator, + ) : VideoWorkProcessor { + + override suspend fun work(command: VideoWorkCommand) = when (command) { + is VideoWorkCommand.LoadVideo -> loadVideoWork() + } + + private fun loadVideoWork() = flow { + videoInfoCommunicator.collect { videoInfo -> + if (videoInfo != null) { + emit(ActionResult(VideoAction.UpdateVideo(videoInfo))) + videoInfoCommunicator.update(null) + } + } + } + } +} + +internal sealed class VideoWorkCommand : WorkCommand { + object LoadVideo : VideoWorkCommand() +} diff --git a/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/views/VideoTopBar.kt b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/views/VideoTopBar.kt new file mode 100644 index 0000000..2ed9704 --- /dev/null +++ b/features/player/impl/src/main/java/ru/aleshin/features/player/impl/presentation/video/views/VideoTopBar.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 + * imitations under the License. + */ + +package ru.aleshin.features.player.impl.presentation.video.views + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import ru.aleshin.core.ui.views.TopAppBarTitle +import ru.aleshin.features.player.impl.presentation.theme.PlayerThemeRes + +/** + * @author Stanislav Aleshin on 13.07.2023. + */ +@Composable +@OptIn(ExperimentalMaterial3Api::class) +internal fun VideoTopBar( + modifier: Modifier = Modifier, + videoName: String?, + onBackPress: () -> Unit, + onOrientationChanged: () -> Unit, +) { + TopAppBar( + modifier = modifier, + title = { + TopAppBarTitle(text = videoName ?: "") + }, + navigationIcon = { + IconButton(onClick = onBackPress) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = PlayerThemeRes.strings.backDesk, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + }, + actions = { + IconButton(onClick = onOrientationChanged) { + Icon( + painter = painterResource(PlayerThemeRes.icons.orientation), + contentDescription = PlayerThemeRes.strings.orientationDesk, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ), + ) +} diff --git a/features/player/impl/src/main/res/drawable/ic_music.xml b/features/player/impl/src/main/res/drawable/ic_music.xml new file mode 100644 index 0000000..4e7f409 --- /dev/null +++ b/features/player/impl/src/main/res/drawable/ic_music.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/player/impl/src/main/res/drawable/ic_next.xml b/features/player/impl/src/main/res/drawable/ic_next.xml new file mode 100644 index 0000000..47b1405 --- /dev/null +++ b/features/player/impl/src/main/res/drawable/ic_next.xml @@ -0,0 +1,5 @@ + + + diff --git a/features/player/impl/src/main/res/drawable/ic_orientation.xml b/features/player/impl/src/main/res/drawable/ic_orientation.xml new file mode 100644 index 0000000..9894e96 --- /dev/null +++ b/features/player/impl/src/main/res/drawable/ic_orientation.xml @@ -0,0 +1,5 @@ + + + diff --git a/features/player/impl/src/main/res/drawable/ic_pause.xml b/features/player/impl/src/main/res/drawable/ic_pause.xml new file mode 100644 index 0000000..938bd7f --- /dev/null +++ b/features/player/impl/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,5 @@ + + + diff --git a/features/player/impl/src/main/res/drawable/ic_play.xml b/features/player/impl/src/main/res/drawable/ic_play.xml new file mode 100644 index 0000000..e3fd2e9 --- /dev/null +++ b/features/player/impl/src/main/res/drawable/ic_play.xml @@ -0,0 +1,5 @@ + + + diff --git a/features/player/impl/src/main/res/drawable/ic_previous.xml b/features/player/impl/src/main/res/drawable/ic_previous.xml new file mode 100644 index 0000000..d915ce0 --- /dev/null +++ b/features/player/impl/src/main/res/drawable/ic_previous.xml @@ -0,0 +1,5 @@ + + + diff --git a/features/player/impl/src/main/res/drawable/ic_volume.xml b/features/player/impl/src/main/res/drawable/ic_volume.xml new file mode 100644 index 0000000..2551246 --- /dev/null +++ b/features/player/impl/src/main/res/drawable/ic_volume.xml @@ -0,0 +1,5 @@ + + + diff --git a/features/player/impl/src/test/java/ru/aleshin/features/player/impl/ExampleUnitTest.kt b/features/player/impl/src/test/java/ru/aleshin/features/player/impl/ExampleUnitTest.kt new file mode 100644 index 0000000..3e46083 --- /dev/null +++ b/features/player/impl/src/test/java/ru/aleshin/features/player/impl/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.authoriztion.impl + +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/settings/api/.gitignore b/features/settings/api/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/features/settings/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/settings/api/build.gradle.kts b/features/settings/api/build.gradle.kts new file mode 100644 index 0000000..8ef9791 --- /dev/null +++ b/features/settings/api/build.gradle.kts @@ -0,0 +1,103 @@ +/* + * 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.settings.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}" + } + } + + kapt { + arguments { + arg("room.schemaLocation", "$projectDir/schemas") + } + } +} + +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.Room.core) + implementation(Dependencies.Room.ktx) + kapt(Dependencies.Room.kapt) + + implementation(Dependencies.Dagger.core) + + testImplementation(Dependencies.Test.jUnit) + androidTestImplementation(Dependencies.Test.jUnitExt) + androidTestImplementation(Dependencies.Test.espresso) +} diff --git a/features/settings/api/consumer-rules.pro b/features/settings/api/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/features/settings/api/proguard-rules.pro b/features/settings/api/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/features/settings/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/settings/api/schemas/ru.aleshin.features.settings.api.data.datasource.SettingsDataBase/1.json b/features/settings/api/schemas/ru.aleshin.features.settings.api.data.datasource.SettingsDataBase/1.json new file mode 100644 index 0000000..d357cf2 --- /dev/null +++ b/features/settings/api/schemas/ru.aleshin.features.settings.api.data.datasource.SettingsDataBase/1.json @@ -0,0 +1,52 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "dcdb0e42e627f857b010dc58b8258e05", + "entities": [ + { + "tableName": "MixPlayerSettings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `language_type` TEXT NOT NULL, `theme_type` TEXT NOT NULL, `volume` REAL NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "languageType", + "columnName": "language_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "themeType", + "columnName": "theme_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volume", + "columnName": "volume", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dcdb0e42e627f857b010dc58b8258e05')" + ] + } +} \ No newline at end of file diff --git a/features/settings/api/schemas/ru.aleshin.features.settings.api.data.datasource.SettingsDataBase/2.json b/features/settings/api/schemas/ru.aleshin.features.settings.api.data.datasource.SettingsDataBase/2.json new file mode 100644 index 0000000..b5aebdd --- /dev/null +++ b/features/settings/api/schemas/ru.aleshin.features.settings.api.data.datasource.SettingsDataBase/2.json @@ -0,0 +1,53 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "66c3c674a8013d06f123faab73e156a3", + "entities": [ + { + "tableName": "MixPlayerSettings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `language_type` TEXT NOT NULL, `theme_type` TEXT NOT NULL, `volume` REAL NOT NULL DEFAULT 1.0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "languageType", + "columnName": "language_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "themeType", + "columnName": "theme_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volume", + "columnName": "volume", + "affinity": "REAL", + "notNull": true, + "defaultValue": "1.0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '66c3c674a8013d06f123faab73e156a3')" + ] + } +} \ No newline at end of file diff --git a/features/settings/api/src/androidTest/java/ru/aleshin/features/settings/api/ExampleInstrumentedTest.kt b/features/settings/api/src/androidTest/java/ru/aleshin/features/settings/api/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..8e89eae --- /dev/null +++ b/features/settings/api/src/androidTest/java/ru/aleshin/features/settings/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.settings.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.settings.api.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/features/settings/api/src/main/AndroidManifest.xml b/features/settings/api/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c193aa3 --- /dev/null +++ b/features/settings/api/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/datasource/SettingsDao.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/datasource/SettingsDao.kt new file mode 100644 index 0000000..33445ba --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/datasource/SettingsDao.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.settings.api.data.datasource + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow +import ru.aleshin.features.settings.api.data.models.MixPlayerSettingsEntity + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +@Dao +interface SettingsDao { + + @Query("SELECT * FROM MixPlayerSettings WHERE id = 0") + fun fetchSettingsFlow(): Flow + + @Query("SELECT * FROM MixPlayerSettings WHERE id = 0") + suspend fun fetchSettings(): MixPlayerSettingsEntity + + @Update(entity = MixPlayerSettingsEntity::class) + suspend fun updateSettings(entity: MixPlayerSettingsEntity) +} diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/datasource/SettingsDataBase.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/datasource/SettingsDataBase.kt new file mode 100644 index 0000000..dd9a1e6 --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/datasource/SettingsDataBase.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.settings.api.data.datasource + +import androidx.room.Database +import androidx.room.RoomDatabase +import ru.aleshin.features.settings.api.data.datasource.SettingsDataBase.Companion.VERSION +import ru.aleshin.features.settings.api.data.models.MixPlayerSettingsEntity + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +@Database( + entities = [MixPlayerSettingsEntity::class], + version = VERSION, + exportSchema = true, +) +abstract class SettingsDataBase : RoomDatabase() { + + abstract fun fetchSettingsDao(): SettingsDao + + companion object { + const val NAME = "mixplayer_settings.db" + const val VERSION = 1 + } +} diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/datasource/SettingsLocalDataSource.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/datasource/SettingsLocalDataSource.kt new file mode 100644 index 0000000..a178a64 --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/datasource/SettingsLocalDataSource.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.settings.api.data.datasource + +import kotlinx.coroutines.flow.Flow +import ru.aleshin.features.settings.api.data.models.MixPlayerSettingsEntity +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023 + */ +interface SettingsLocalDataSource { + + fun fetchSettingsFlow(): Flow + suspend fun fetchSettings(): MixPlayerSettingsEntity + suspend fun updateSettings(entity: MixPlayerSettingsEntity) + + class Base @Inject constructor( + private val settingsDao: SettingsDao, + ) : SettingsLocalDataSource { + + override fun fetchSettingsFlow(): Flow { + return settingsDao.fetchSettingsFlow() + } + + override suspend fun fetchSettings(): MixPlayerSettingsEntity { + return settingsDao.fetchSettings() + } + + override suspend fun updateSettings(entity: MixPlayerSettingsEntity) { + settingsDao.updateSettings(entity) + } + } +} diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/mappers/SettingsDataMappers.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/mappers/SettingsDataMappers.kt new file mode 100644 index 0000000..bfbf044 --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/mappers/SettingsDataMappers.kt @@ -0,0 +1,41 @@ +/* + * 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.settings.api.data.mappers + +import ru.aleshin.features.settings.api.data.models.MixPlayerSettingsEntity +import ru.aleshin.features.settings.api.domain.entities.MixPlayerSettings +import ru.aleshin.features.settings.api.domain.entities.GeneralSettings +import ru.aleshin.features.settings.api.domain.entities.PlayerSettings + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +fun MixPlayerSettingsEntity.mapToDomain() = MixPlayerSettings( + general = GeneralSettings( + languageType = languageType, + themeType = themeType, + ), + player = PlayerSettings( + volume = volume, + ) +) + +fun MixPlayerSettings.mapToData() = MixPlayerSettingsEntity( + languageType = general.languageType, + themeType = general.themeType, + volume = player.volume, +) diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/models/MixPlayerSettingsEntity.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/models/MixPlayerSettingsEntity.kt new file mode 100644 index 0000000..47115d8 --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/models/MixPlayerSettingsEntity.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.settings.api.data.models + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import ru.aleshin.features.settings.api.domain.entities.LanguageType +import ru.aleshin.features.settings.api.domain.entities.ThemeType + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +@Entity(tableName = "MixPlayerSettings") +data class MixPlayerSettingsEntity( + @PrimaryKey(autoGenerate = false) val id: Int = 0, + @ColumnInfo(name = "language_type") val languageType: LanguageType, + @ColumnInfo(name = "theme_type") val themeType: ThemeType, + @ColumnInfo(name = "volume") val volume: Float, +) diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/repositories/SettingsRepositoryImpl.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/repositories/SettingsRepositoryImpl.kt new file mode 100644 index 0000000..856dcb8 --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/data/repositories/SettingsRepositoryImpl.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.settings.api.data.repositories + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import ru.aleshin.features.settings.api.data.datasource.SettingsLocalDataSource +import ru.aleshin.features.settings.api.data.mappers.mapToData +import ru.aleshin.features.settings.api.data.mappers.mapToDomain +import ru.aleshin.features.settings.api.domain.entities.MixPlayerSettings +import ru.aleshin.features.settings.api.domain.repositories.SettingsRepository +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +class SettingsRepositoryImpl @Inject constructor( + private val localDataSource: SettingsLocalDataSource, +) : SettingsRepository { + + override fun fetchSettingsFlow(): Flow { + return localDataSource.fetchSettingsFlow().map { settings -> settings.mapToDomain() } + } + + override suspend fun fetchSettings(): MixPlayerSettings { + return localDataSource.fetchSettings().mapToDomain() + } + + override suspend fun updateSettings(model: MixPlayerSettings) { + localDataSource.updateSettings(model.mapToData()) + } +} diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/di/SettingsFeatureApi.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/di/SettingsFeatureApi.kt new file mode 100644 index 0000000..281aedd --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/di/SettingsFeatureApi.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.settings.api.di + +import ru.aleshin.features.settings.api.navigation.SettingsFeatureStarter +import ru.aleshin.module_injector.BaseFeatureApi + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +interface SettingsFeatureApi : BaseFeatureApi { + fun fetchStarter(): SettingsFeatureStarter +} diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/GeneralSettings.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/GeneralSettings.kt new file mode 100644 index 0000000..9431038 --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/GeneralSettings.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.settings.api.domain.entities + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +data class GeneralSettings( + val languageType: LanguageType = LanguageType.DEFAULT, + val themeType: ThemeType = ThemeType.DEFAULT, +) diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/LanguageType.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/LanguageType.kt new file mode 100644 index 0000000..0bcb818 --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/LanguageType.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 + * imitations under the License. + */ + +package ru.aleshin.features.settings.api.domain.entities + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +enum class LanguageType { + DEFAULT, EN, RU +} diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/MixPlayerSettings.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/MixPlayerSettings.kt new file mode 100644 index 0000000..e9bcebf --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/MixPlayerSettings.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.settings.api.domain.entities + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +data class MixPlayerSettings( + val general: GeneralSettings = GeneralSettings(), + val player: PlayerSettings = PlayerSettings(), +) diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/PlayerSettings.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/PlayerSettings.kt new file mode 100644 index 0000000..a77c0b1 --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/PlayerSettings.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 + * imitations under the License. + */ + +package ru.aleshin.features.settings.api.domain.entities + +/** + * @author Stanislav Aleshin on 14.07.2023. + */ +data class PlayerSettings( + val volume: Float = 1f, +) diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/ThemeType.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/ThemeType.kt new file mode 100644 index 0000000..7130629 --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/entities/ThemeType.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 + * imitations under the License. + */ + +package ru.aleshin.features.settings.api.domain.entities + +/** + * @author Stanislav Aleshin on 01.07.2023. + */ +enum class ThemeType { + DEFAULT, LIGHT, DARK +} diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/repositories/SettingsRepository.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/repositories/SettingsRepository.kt new file mode 100644 index 0000000..f98d755 --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/domain/repositories/SettingsRepository.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.features.settings.api.domain.repositories + +import kotlinx.coroutines.flow.Flow +import ru.aleshin.features.settings.api.domain.entities.MixPlayerSettings + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +interface SettingsRepository { + fun fetchSettingsFlow(): Flow + suspend fun fetchSettings(): MixPlayerSettings + suspend fun updateSettings(model: MixPlayerSettings) +} diff --git a/features/settings/api/src/main/java/ru/aleshin/features/settings/api/navigation/SettingsFeatureStarter.kt b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/navigation/SettingsFeatureStarter.kt new file mode 100644 index 0000000..94f3dee --- /dev/null +++ b/features/settings/api/src/main/java/ru/aleshin/features/settings/api/navigation/SettingsFeatureStarter.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.settings.api.navigation + +import cafe.adriel.voyager.core.screen.Screen + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +interface SettingsFeatureStarter { + fun fetchMainScreen(): Screen +} diff --git a/features/settings/api/src/test/java/ru/aleshin/features/settings/api/ExampleUnitTest.kt b/features/settings/api/src/test/java/ru/aleshin/features/settings/api/ExampleUnitTest.kt new file mode 100644 index 0000000..034f6f1 --- /dev/null +++ b/features/settings/api/src/test/java/ru/aleshin/features/settings/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.settings.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/settings/impl/.gitignore b/features/settings/impl/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/features/settings/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/settings/impl/build.gradle.kts b/features/settings/impl/build.gradle.kts new file mode 100644 index 0000000..179ee19 --- /dev/null +++ b/features/settings/impl/build.gradle.kts @@ -0,0 +1,104 @@ +/* + * 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.settings.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:settings:api")) + + implementation(Dependencies.AndroidX.core) + implementation(Dependencies.AndroidX.appcompat) + implementation(Dependencies.AndroidX.lifecycleRuntime) + implementation(Dependencies.AndroidX.material) + implementation(Dependencies.AndroidX.placeHolder) + + implementation(Dependencies.Compose.ui) + implementation(Dependencies.Compose.activity) + + 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/settings/impl/consumer-rules.pro b/features/settings/impl/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/features/settings/impl/proguard-rules.pro b/features/settings/impl/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/features/settings/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/settings/impl/src/androidTest/java/ru/aleshin/features/settings/impl/ExampleInstrumentedTest.kt b/features/settings/impl/src/androidTest/java/ru/aleshin/features/settings/impl/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..3bcba80 --- /dev/null +++ b/features/settings/impl/src/androidTest/java/ru/aleshin/features/settings/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.settings.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.settings.impl.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/features/settings/impl/src/main/AndroidManifest.xml b/features/settings/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c193aa3 --- /dev/null +++ b/features/settings/impl/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/SettingsFeatureDependencies.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/SettingsFeatureDependencies.kt new file mode 100644 index 0000000..e7effe7 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/SettingsFeatureDependencies.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.settings.impl.di + +import ru.aleshin.core.common.managers.CoroutineManager +import ru.aleshin.core.common.navigation.Router +import ru.aleshin.features.settings.api.domain.repositories.SettingsRepository +import ru.aleshin.module_injector.BaseFeatureDependencies + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +interface SettingsFeatureDependencies : BaseFeatureDependencies { + val globalRouter: Router + val settingsRepository: SettingsRepository + val coroutineManager: CoroutineManager +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/component/SettingsComponent.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/component/SettingsComponent.kt new file mode 100644 index 0000000..f731beb --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/component/SettingsComponent.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. + */ + +package ru.aleshin.features.settings.impl.di.component + +import dagger.Component +import ru.aleshin.core.common.di.FeatureScope +import ru.aleshin.features.settings.api.di.SettingsFeatureApi +import ru.aleshin.features.settings.impl.di.SettingsFeatureDependencies +import ru.aleshin.features.settings.impl.di.modules.DomainModule +import ru.aleshin.features.settings.impl.di.modules.PresentationModule +import ru.aleshin.features.settings.impl.presentation.settings.screenmodel.SettingsScreenModel + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +@FeatureScope +@Component( + modules = [DomainModule::class, PresentationModule::class], + dependencies = [SettingsFeatureDependencies::class], +) +internal interface SettingsComponent : SettingsFeatureApi { + + fun fetchSettingsScreenModel(): SettingsScreenModel + + @Component.Builder + interface Builder { + fun dependencies(deps: SettingsFeatureDependencies): Builder + fun build(): SettingsComponent + } + + companion object { + fun create(deps: SettingsFeatureDependencies): SettingsComponent { + return DaggerSettingsComponent.builder() + .dependencies(deps) + .build() + } + } +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/holder/SettingsComponentHolder.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/holder/SettingsComponentHolder.kt new file mode 100644 index 0000000..2a274e7 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/holder/SettingsComponentHolder.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.settings.impl.di.holder + +import ru.aleshin.features.settings.api.di.SettingsFeatureApi +import ru.aleshin.features.settings.impl.di.SettingsFeatureDependencies +import ru.aleshin.features.settings.impl.di.component.SettingsComponent +import ru.aleshin.module_injector.BaseComponentHolder + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +object SettingsComponentHolder : BaseComponentHolder { + + private var component: SettingsComponent? = null + + override fun init(dependencies: SettingsFeatureDependencies) { + if (component == null) component = SettingsComponent.create(dependencies) + } + + override fun clear() { + component = null + } + + override fun fetchApi(): SettingsFeatureApi { + return fetchComponent() + } + + internal fun fetchComponent() = checkNotNull(component) { + "Settings Component is not initialized" + } +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/modules/DomainModule.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/modules/DomainModule.kt new file mode 100644 index 0000000..5b3f4b2 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/modules/DomainModule.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.settings.impl.di.modules + +import dagger.Binds +import dagger.Module +import ru.aleshin.core.common.di.FeatureScope +import ru.aleshin.features.settings.impl.domain.common.SettingsEitherWrapper +import ru.aleshin.features.settings.impl.domain.common.SettingsErrorHandler +import ru.aleshin.features.settings.impl.domain.interactors.SettingsInteractor + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +@Module +internal interface DomainModule { + + @Binds + @FeatureScope + fun bindSettingsErrorHandler(handler: SettingsErrorHandler.Base): SettingsErrorHandler + + @Binds + @FeatureScope + fun bindSettingsEitherWrapper(wrapper: SettingsEitherWrapper.Base): SettingsEitherWrapper + + @Binds + fun bindSettingsInteractor(interactor: SettingsInteractor.Base): SettingsInteractor +} \ No newline at end of file diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/modules/PresentationModule.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/modules/PresentationModule.kt new file mode 100644 index 0000000..f5e51c8 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/di/modules/PresentationModule.kt @@ -0,0 +1,67 @@ +/* + * 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.settings.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.features.settings.api.navigation.SettingsFeatureStarter +import ru.aleshin.features.settings.impl.navigation.SettingsFeatureStarterImpl +import ru.aleshin.features.settings.impl.navigation.SettingsNavigationManager +import ru.aleshin.features.settings.impl.presentation.settings.SettingsScreen +import ru.aleshin.features.settings.impl.presentation.settings.screenmodel.SettingsEffectCommunicator +import ru.aleshin.features.settings.impl.presentation.settings.screenmodel.SettingsScreenModel +import ru.aleshin.features.settings.impl.presentation.settings.screenmodel.SettingsStateCommunicator +import ru.aleshin.features.settings.impl.presentation.settings.screenmodel.SettingsWorkProcessor + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +@Module +internal interface PresentationModule { + + // Common + + @Binds + @FeatureScope + fun bindSettingsScreen(screen: SettingsScreen): Screen + + @Binds + @FeatureScope + fun bindSettingsFeatureStarter(starter: SettingsFeatureStarterImpl): SettingsFeatureStarter + + @Binds + @FeatureScope + fun bindSettingsNavigationManager(manager: SettingsNavigationManager.Base): SettingsNavigationManager + + // Settings + + @Binds + fun bindSettingsScreenModel(screenModel: SettingsScreenModel): ScreenModel + + @Binds + @FeatureScope + fun bindSettingsStateCommunicator(communicator: SettingsStateCommunicator.Base): SettingsStateCommunicator + + @Binds + fun bindSettingsEffectCommunicator(communicator: SettingsEffectCommunicator.Base): SettingsEffectCommunicator + + @Binds + fun bindSettingsWorkProcessor(processor: SettingsWorkProcessor.Base): SettingsWorkProcessor +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/common/SettingsEitherWrapper.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/common/SettingsEitherWrapper.kt new file mode 100644 index 0000000..c365b26 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/common/SettingsEitherWrapper.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.settings.impl.domain.common + +import ru.aleshin.core.common.wrappers.EitherWrapper +import ru.aleshin.core.common.wrappers.FlowEitherWrapper +import ru.aleshin.features.settings.impl.domain.entities.SettingsFailures +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 12.07.2023 + */ +internal interface SettingsEitherWrapper : FlowEitherWrapper { + + class Base @Inject constructor( + errorHandler: SettingsErrorHandler, + ) : SettingsEitherWrapper, FlowEitherWrapper.Abstract(errorHandler) +} \ No newline at end of file diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/common/SettingsErrorHandler.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/common/SettingsErrorHandler.kt new file mode 100644 index 0000000..64b897f --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/common/SettingsErrorHandler.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.settings.impl.domain.common + +import ru.aleshin.core.common.handlers.ErrorHandler +import ru.aleshin.features.settings.impl.domain.entities.SettingsFailures +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 12.07.2023 + */ +internal interface SettingsErrorHandler : ErrorHandler { + + class Base @Inject constructor() : SettingsErrorHandler { + + override fun handle(throwable: Throwable) = when (throwable) { + else -> SettingsFailures.OtherError(throwable) + } + } +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/entities/SettingsFailures.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/entities/SettingsFailures.kt new file mode 100644 index 0000000..98b5d76 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/entities/SettingsFailures.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.settings.impl.domain.entities + +import ru.aleshin.core.common.functional.DomainFailures + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +internal sealed class SettingsFailures : DomainFailures { + data class OtherError(val throwable: Throwable) : SettingsFailures() +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/interactors/SettingsInteractor.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/interactors/SettingsInteractor.kt new file mode 100644 index 0000000..eb292e2 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/domain/interactors/SettingsInteractor.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.features.settings.impl.domain.interactors + +import kotlinx.coroutines.flow.Flow +import ru.aleshin.core.common.functional.DomainResult +import ru.aleshin.core.common.functional.UnitDomainResult +import ru.aleshin.features.settings.api.domain.entities.MixPlayerSettings +import ru.aleshin.features.settings.api.domain.entities.GeneralSettings +import ru.aleshin.features.settings.api.domain.repositories.SettingsRepository +import ru.aleshin.features.settings.impl.domain.common.SettingsEitherWrapper +import ru.aleshin.features.settings.impl.domain.entities.SettingsFailures +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +internal interface SettingsInteractor { + + suspend fun fetchSettings(): Flow> + suspend fun updateGeneralSettings(general: GeneralSettings): UnitDomainResult + + class Base @Inject constructor( + private val settingsRepository: SettingsRepository, + private val eitherWrapper: SettingsEitherWrapper, + ) : SettingsInteractor { + + override suspend fun fetchSettings() = eitherWrapper.wrapFlow { + settingsRepository.fetchSettingsFlow() + } + + override suspend fun updateGeneralSettings(general: GeneralSettings) = eitherWrapper.wrap { + val settings = settingsRepository.fetchSettings() + settingsRepository.updateSettings(settings.copy(general = general)) + } + } +} \ No newline at end of file diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/navigation/SettingsFeatureStarterImpl.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/navigation/SettingsFeatureStarterImpl.kt new file mode 100644 index 0000000..d629e03 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/navigation/SettingsFeatureStarterImpl.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.settings.impl.navigation + +import cafe.adriel.voyager.core.screen.Screen +import ru.aleshin.features.settings.api.navigation.SettingsFeatureStarter +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +internal class SettingsFeatureStarterImpl @Inject constructor( + private val settingsScreen: Screen, +) : SettingsFeatureStarter { + + override fun fetchMainScreen() = settingsScreen +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/navigation/SettingsNavigationManager.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/navigation/SettingsNavigationManager.kt new file mode 100644 index 0000000..697fe3f --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/navigation/SettingsNavigationManager.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.settings.impl.navigation + +import ru.aleshin.core.common.navigation.Router +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 17.06.2023. + */ +interface SettingsNavigationManager { + + fun navigateToBack() + + class Base @Inject constructor(private val globalRouter: Router) : SettingsNavigationManager { + + override fun navigateToBack() { + globalRouter.navigateBack() + } + } +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/mappers/FailureMapper.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/mappers/FailureMapper.kt new file mode 100644 index 0000000..d3b4bb6 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/mappers/FailureMapper.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.settings.impl.presentation.mappers + +import ru.aleshin.features.settings.impl.domain.entities.SettingsFailures +import ru.aleshin.features.settings.impl.presentation.theme.tokens.SettingsStrings + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +internal fun SettingsFailures.mapToMessage(strings: SettingsStrings) = when (this) { + is SettingsFailures.OtherError -> strings.otherError +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/mappers/GeneralSettingsMappers.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/mappers/GeneralSettingsMappers.kt new file mode 100644 index 0000000..2e0beff --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/mappers/GeneralSettingsMappers.kt @@ -0,0 +1,65 @@ +/* + * 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.settings.impl.presentation.mappers + +import androidx.compose.runtime.Composable +import ru.aleshin.core.ui.theme.material.ThemeUiType +import ru.aleshin.core.ui.theme.tokens.LanguageUiType +import ru.aleshin.features.settings.api.domain.entities.LanguageType +import ru.aleshin.features.settings.api.domain.entities.ThemeType +import ru.aleshin.features.settings.impl.presentation.theme.SettingsThemeRes + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +internal fun LanguageUiType.mapToDomain() = when (this) { + LanguageUiType.DEFAULT -> LanguageType.DEFAULT + LanguageUiType.EN -> LanguageType.EN + LanguageUiType.RU -> LanguageType.RU +} + +@Composable +internal fun LanguageUiType.mapToMessage() = when (this) { + LanguageUiType.DEFAULT -> SettingsThemeRes.strings.defaultTitle + LanguageUiType.EN -> SettingsThemeRes.strings.englishLanguage + LanguageUiType.RU -> SettingsThemeRes.strings.russianLanguage +} + +internal fun ThemeUiType.mapToDomain() = when (this) { + ThemeUiType.DEFAULT -> ThemeType.DEFAULT + ThemeUiType.LIGHT -> ThemeType.LIGHT + ThemeUiType.DARK -> ThemeType.DARK +} + +@Composable +internal fun ThemeUiType.mapToMessage() = when (this) { + ThemeUiType.DEFAULT -> SettingsThemeRes.strings.defaultTitle + ThemeUiType.LIGHT -> SettingsThemeRes.strings.lightTheme + ThemeUiType.DARK -> SettingsThemeRes.strings.darkTheme +} + +internal fun LanguageType.mapToUi() = when (this) { + LanguageType.DEFAULT -> LanguageUiType.DEFAULT + LanguageType.EN -> LanguageUiType.EN + LanguageType.RU -> LanguageUiType.RU +} + +internal fun ThemeType.mapToUi() = when (this) { + ThemeType.DEFAULT -> ThemeUiType.DEFAULT + ThemeType.LIGHT -> ThemeUiType.LIGHT + ThemeType.DARK -> ThemeUiType.DARK +} \ No newline at end of file diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/mappers/SettingsMappers.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/mappers/SettingsMappers.kt new file mode 100644 index 0000000..62a427a --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/mappers/SettingsMappers.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.settings.impl.presentation.mappers + +import ru.aleshin.features.settings.api.domain.entities.GeneralSettings +import ru.aleshin.features.settings.impl.presentation.models.GeneralSettingsUi + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +internal fun GeneralSettingsUi.mapToDomain() = GeneralSettings( + languageType = languageUiType.mapToDomain(), + themeType = themeUiType.mapToDomain(), +) + +internal fun GeneralSettings.mapToUi() = GeneralSettingsUi( + languageUiType = languageType.mapToUi(), + themeUiType = themeType.mapToUi(), +) \ No newline at end of file diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/models/GeneralSettingsUi.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/models/GeneralSettingsUi.kt new file mode 100644 index 0000000..c8f1c70 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/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.features.settings.impl.presentation.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 15.06.2023. + */ +@Parcelize +internal data class GeneralSettingsUi( + val languageUiType: LanguageUiType = LanguageUiType.DEFAULT, + val themeUiType: ThemeUiType = ThemeUiType.DEFAULT, +) : Parcelable diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/SettingsContent.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/SettingsContent.kt new file mode 100644 index 0000000..6645d3e --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/SettingsContent.kt @@ -0,0 +1,132 @@ +/* + * 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.settings.impl.presentation.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import ru.aleshin.core.ui.theme.MixPlayerTheme +import ru.aleshin.core.ui.theme.material.ThemeUiType +import ru.aleshin.features.settings.impl.presentation.mappers.mapToMessage +import ru.aleshin.features.settings.impl.presentation.models.GeneralSettingsUi +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsViewState +import ru.aleshin.features.settings.impl.presentation.settings.views.LanguageChooserDialog +import ru.aleshin.features.settings.impl.presentation.settings.views.SettingsItem +import ru.aleshin.features.settings.impl.presentation.settings.views.ThemeChooserDialog +import ru.aleshin.features.settings.impl.presentation.theme.SettingsTheme +import ru.aleshin.features.settings.impl.presentation.theme.SettingsThemeRes + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +@Composable +internal fun SettingsContent( + state: SettingsViewState, + modifier: Modifier = Modifier, + onChangeGeneralSettings: (GeneralSettingsUi?) -> Unit, +) { + val scrollState = rememberScrollState() + Column(modifier = modifier.verticalScroll(scrollState)) { + Divider() + GeneralSettingsSection( + generalSettingsUi = state.generalSettings, + onChangeSettings = onChangeGeneralSettings, + ) + } +} + +@Composable +internal fun GeneralSettingsSection( + modifier: Modifier = Modifier, + generalSettingsUi: GeneralSettingsUi?, + onChangeSettings: (GeneralSettingsUi?) -> Unit, +) { + var isOpenThemeDialog by rememberSaveable { mutableStateOf(false) } + var isOpenLanguageDialog by rememberSaveable { mutableStateOf(false) } + + Column( + modifier = modifier.padding(bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp), + text = SettingsThemeRes.strings.generalSettingsHeader, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleSmall, + ) + SettingsItem( + onClick = { isOpenThemeDialog = true }, + icon = painterResource(id = SettingsThemeRes.icons.palette), + title = SettingsThemeRes.strings.themeTitle, + value = generalSettingsUi?.themeUiType?.mapToMessage(), + ) + SettingsItem( + onClick = { isOpenLanguageDialog = true }, + icon = painterResource(id = SettingsThemeRes.icons.language), + title = SettingsThemeRes.strings.appLanguageTitle, + value = generalSettingsUi?.languageUiType?.mapToMessage(), + ) + Divider() + } + + if (isOpenThemeDialog) { + ThemeChooserDialog( + initTheme = checkNotNull(generalSettingsUi).themeUiType, + onCloseDialog = { isOpenThemeDialog = false }, + onChooseTheme = { + onChangeSettings(generalSettingsUi.copy(themeUiType = it)) + isOpenThemeDialog = false + }, + ) + } + if (isOpenLanguageDialog) { + LanguageChooserDialog( + initLanguage = checkNotNull(generalSettingsUi).languageUiType, + onCloseDialog = { isOpenLanguageDialog = false }, + onChooseLanguage = { + onChangeSettings(generalSettingsUi.copy(languageUiType = it)) + isOpenLanguageDialog = false + }, + ) + } +} + +//@Composable +//@Preview(showBackground = true) +//private fun SettingsContent_Preview() { +// MixPlayerTheme(themeType = ThemeUiType.LIGHT) { +// SettingsTheme { +// SettingsContent( +// state = SettingsViewState(GeneralSettingsUi()), +// onChangeGeneralSettings = {}, +// ) +// } +// } +//} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/SettingsScreen.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/SettingsScreen.kt new file mode 100644 index 0000000..52268c4 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/SettingsScreen.kt @@ -0,0 +1,85 @@ +/* + * 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.settings.impl.presentation.settings + +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.settings.impl.presentation.mappers.mapToMessage +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsEffect +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsEvent +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsViewState +import ru.aleshin.features.settings.impl.presentation.settings.screenmodel.rememberSettingsScreenModel +import ru.aleshin.features.settings.impl.presentation.settings.views.SettingsTopBar +import ru.aleshin.features.settings.impl.presentation.theme.SettingsTheme +import ru.aleshin.features.settings.impl.presentation.theme.SettingsThemeRes +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023 + */ +internal class SettingsScreen @Inject constructor() : Screen { + + @Composable + override fun Content() = ScreenContent( + screenModel = rememberSettingsScreenModel(), + initialState = SettingsViewState(), + ) { state -> + SettingsTheme { + val strings = SettingsThemeRes.strings + val snackbarState = remember { SnackbarHostState() } + + Scaffold( + modifier = Modifier.fillMaxSize(), + content = { paddingValues -> + SettingsContent( + state = state, + modifier = Modifier.padding(paddingValues), + onChangeGeneralSettings = { dispatchEvent(SettingsEvent.ChangedGeneralSettings(it)) }, + ) + }, + topBar = { + SettingsTopBar( + onBackPress = { dispatchEvent(SettingsEvent.PressBackButton) }, + onResetClick = { dispatchEvent(SettingsEvent.PressResetButton) }, + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarState) + }, + ) + + handleEffect { effect -> + when (effect) { + is SettingsEffect.ShowError -> { + snackbarState.showSnackbar( + message = effect.failures.mapToMessage(strings), + withDismissAction = true, + ) + } + } + } + } + } +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/contract/SettingsContract.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/contract/SettingsContract.kt new file mode 100644 index 0000000..8613b5a --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/contract/SettingsContract.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.settings.impl.presentation.settings.contract + +import kotlinx.parcelize.Parcelize +import ru.aleshin.core.common.platform.screenmodel.contract.* +import ru.aleshin.features.settings.impl.domain.entities.SettingsFailures +import ru.aleshin.features.settings.impl.presentation.models.GeneralSettingsUi + +/** + * @author Stanislav Aleshin on 15.06.2023 + */ +@Parcelize +internal data class SettingsViewState( + val generalSettings: GeneralSettingsUi? = null, +) : BaseViewState + +internal sealed class SettingsEvent : BaseEvent { + object Init : SettingsEvent() + object PressBackButton : SettingsEvent() + object PressResetButton : SettingsEvent() + data class ChangedGeneralSettings(val settings: GeneralSettingsUi?) : SettingsEvent() +} + +internal sealed class SettingsEffect : BaseUiEffect { + data class ShowError(val failures: SettingsFailures) : SettingsEffect() +} + +internal sealed class SettingsAction : BaseAction { + object Navigate : SettingsAction() + data class UpdateSettings(val general: GeneralSettingsUi?) : SettingsAction() +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsEffectCommunicator.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsEffectCommunicator.kt new file mode 100644 index 0000000..932cf5a --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsEffectCommunicator.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.settings.impl.presentation.settings.screenmodel + +import ru.aleshin.core.common.platform.communications.state.EffectCommunicator +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsEffect +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +internal interface SettingsEffectCommunicator : EffectCommunicator { + + class Base @Inject constructor() : SettingsEffectCommunicator, + EffectCommunicator.Abstract() +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsScreenModel.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsScreenModel.kt new file mode 100644 index 0000000..bcd1a28 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsScreenModel.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.features.settings.impl.presentation.settings.screenmodel + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import ru.aleshin.core.common.managers.CoroutineManager +import ru.aleshin.core.common.platform.screenmodel.BaseScreenModel +import ru.aleshin.core.common.platform.screenmodel.work.WorkScope +import ru.aleshin.features.settings.impl.di.holder.SettingsComponentHolder +import ru.aleshin.features.settings.impl.navigation.SettingsNavigationManager +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsAction +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsEffect +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsEvent +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsViewState +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023 + */ +internal class SettingsScreenModel @Inject constructor( + private val settingsWorkProcessor: SettingsWorkProcessor, + private val navigationManager: SettingsNavigationManager, + stateCommunicator: SettingsStateCommunicator, + effectCommunicator: SettingsEffectCommunicator, + coroutineManager: CoroutineManager, +) : BaseScreenModel( + stateCommunicator = stateCommunicator, + effectCommunicator = effectCommunicator, + coroutineManager = coroutineManager, +) { + + override fun init() { + if (!isInitialize.get()) { + super.init() + dispatchEvent(SettingsEvent.Init) + } + } + + override suspend fun WorkScope.handleEvent( + event: SettingsEvent, + ) { + when (event) { + is SettingsEvent.Init -> launchBackgroundWork(SettingsWorkCommand.LoadSettings) { + settingsWorkProcessor.work(SettingsWorkCommand.LoadSettings).collectAndHandleWork() + } + is SettingsEvent.ChangedGeneralSettings -> { + val settings = checkNotNull(event.settings) + settingsWorkProcessor.work(SettingsWorkCommand.ChangeGeneralSettings(settings)).collectAndHandleWork() + } + is SettingsEvent.PressResetButton -> {} + is SettingsEvent.PressBackButton -> navigationManager.navigateToBack() + } + } + + override suspend fun reduce( + action: SettingsAction, + currentState: SettingsViewState, + ) = when (action) { + is SettingsAction.UpdateSettings -> currentState.copy( + generalSettings = action.general, + ) + is SettingsAction.Navigate -> currentState.copy() + } +} + +@Composable +internal fun Screen.rememberSettingsScreenModel(): SettingsScreenModel { + val component = SettingsComponentHolder.fetchComponent() + return rememberScreenModel { component.fetchSettingsScreenModel() } +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsStateCommunicator.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsStateCommunicator.kt new file mode 100644 index 0000000..5e02399 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsStateCommunicator.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.settings.impl.presentation.settings.screenmodel + +import ru.aleshin.core.common.platform.communications.state.StateCommunicator +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsViewState +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +internal interface SettingsStateCommunicator : StateCommunicator { + + class Base @Inject constructor() : SettingsStateCommunicator, + StateCommunicator.Abstract(defaultState = SettingsViewState()) +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsWorkProcessor.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsWorkProcessor.kt new file mode 100644 index 0000000..2e18709 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/screenmodel/SettingsWorkProcessor.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.features.settings.impl.presentation.settings.screenmodel + +import kotlinx.coroutines.flow.flow +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.features.settings.impl.domain.interactors.SettingsInteractor +import ru.aleshin.features.settings.impl.presentation.mappers.mapToDomain +import ru.aleshin.features.settings.impl.presentation.mappers.mapToUi +import ru.aleshin.features.settings.impl.presentation.models.GeneralSettingsUi +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsAction +import ru.aleshin.features.settings.impl.presentation.settings.contract.SettingsEffect +import javax.inject.Inject + +/** + * @author Stanislav Aleshin on 11.07.2023. + */ +internal interface SettingsWorkProcessor : FlowWorkProcessor { + + class Base @Inject constructor( + private val settingsInteractor: SettingsInteractor, + ) : SettingsWorkProcessor { + + override suspend fun work(command: SettingsWorkCommand) = when (command) { + is SettingsWorkCommand.LoadSettings -> loadSettingsWork() + is SettingsWorkCommand.ChangeGeneralSettings -> changeGeneralSettingsWork(command.settings) + } + + private fun changeGeneralSettingsWork(settings: GeneralSettingsUi) = flow { + settingsInteractor.updateGeneralSettings(settings.mapToDomain()).handle( + onLeftAction = { emit(EffectResult(SettingsEffect.ShowError(it))) } + ) + } + + private fun loadSettingsWork() = flow { + settingsInteractor.fetchSettings().collect { settingsEither -> + settingsEither.handle( + onRightAction = { + emit(ActionResult(SettingsAction.UpdateSettings(general = it.general.mapToUi()))) + }, + onLeftAction = { emit(EffectResult(SettingsEffect.ShowError(it))) } + ) + } + } + } +} + +internal sealed class SettingsWorkCommand : WorkCommand { + object LoadSettings : SettingsWorkCommand() + data class ChangeGeneralSettings(val settings: GeneralSettingsUi) : SettingsWorkCommand() +} \ No newline at end of file diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/LanguageChooserDialog.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/LanguageChooserDialog.kt new file mode 100644 index 0000000..1e33378 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/LanguageChooserDialog.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 + * imitations under the License. + */ + +package ru.aleshin.features.settings.impl.presentation.settings.views + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ru.aleshin.core.ui.theme.tokens.LanguageUiType +import ru.aleshin.core.ui.views.DialogButtons +import ru.aleshin.features.settings.impl.presentation.mappers.mapToMessage +import ru.aleshin.features.settings.impl.presentation.theme.SettingsThemeRes + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +@Composable +@OptIn(ExperimentalMaterial3Api::class) +internal fun LanguageChooserDialog( + modifier: Modifier = Modifier, + initLanguage: LanguageUiType, + onCloseDialog: () -> Unit, + onChooseLanguage: (LanguageUiType) -> Unit, +) { + var currentLanguage by remember { mutableStateOf(initLanguage) } + + AlertDialog(onDismissRequest = onCloseDialog) { + Surface( + modifier = modifier.width(280.dp).wrapContentHeight(), + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + Column { + Box(modifier = Modifier.padding(24.dp)) { + Text( + text = SettingsThemeRes.strings.appLanguageTitle, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.headlineSmall, + ) + } + LazyColumn(modifier = Modifier.height(300.dp)) { + items(LanguageUiType.values()) { language -> + SettingsDialogItem( + modifier = Modifier.fillMaxWidth(), + selected = language == currentLanguage, + title = language.mapToMessage(), + onSelectChange = { currentLanguage = language }, + ) + } + } + DialogButtons( + onCancelClick = onCloseDialog, + onConfirmClick = { onChooseLanguage(currentLanguage) }, + ) + } + } + } +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/SettingsDialogItem.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/SettingsDialogItem.kt new file mode 100644 index 0000000..8d4aab0 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/SettingsDialogItem.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.features.settings.impl.presentation.settings.views + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +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.unit.dp + +@Composable +internal fun SettingsDialogItem( + modifier: Modifier = Modifier, + selected: Boolean, + title: String, + onSelectChange: () -> Unit, +) { + Column { + Row( + modifier = modifier + .padding(vertical = 8.dp, horizontal = 8.dp) + .clip(MaterialTheme.shapes.medium) + .clickable(onClick = onSelectChange) + .padding(start = 8.dp, end = 16.dp) + .requiredHeight(56.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton(selected = selected, onClick = null) + Text( + modifier = Modifier.weight(1f), + text = title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + ) + } + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + color = MaterialTheme.colorScheme.outlineVariant, + ) + } +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/SettingsItem.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/SettingsItem.kt new file mode 100644 index 0000000..0cc153a --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/SettingsItem.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.features.settings.impl.presentation.settings.views + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.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.painter.Painter +import androidx.compose.ui.unit.dp + +/** + * @author Stanislav Aleshin on 11.07.2023. + */ +@Composable +internal fun SettingsItem( + modifier: Modifier = Modifier, + onClick: () -> Unit, + icon: Painter, + title: String, + value: String?, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 2.dp, horizontal = 8.dp) + .clip(MaterialTheme.shapes.medium) + .clickable(enabled = value != null, onClick = onClick) + .padding(vertical = 6.dp, horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = icon, + contentDescription = title, + tint = MaterialTheme.colorScheme.primary, + ) + Column { + Text( + text = title, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelMedium, + ) + Text( + text = value ?: "", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + ) + } + } +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/SettingsTopBar.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/SettingsTopBar.kt new file mode 100644 index 0000000..192548c --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/SettingsTopBar.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.features.settings.impl.presentation.settings.views + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import ru.aleshin.core.ui.views.TopAppBarAction +import ru.aleshin.core.ui.views.TopAppBarMoreActions +import ru.aleshin.core.ui.views.TopAppBarTitle +import ru.aleshin.features.settings.impl.presentation.theme.SettingsThemeRes + +/** + * @author Stanislav Aleshin on 15.06.2023. + */ +@Composable +@OptIn(ExperimentalMaterial3Api::class) +internal fun SettingsTopBar( + modifier: Modifier = Modifier, + onBackPress: () -> Unit, + onResetClick: () -> Unit, +) { + TopAppBar( + modifier = modifier, + title = { + TopAppBarTitle(text = SettingsThemeRes.strings.settingsHeader) + }, + navigationIcon = { + IconButton(onClick = onBackPress) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = SettingsThemeRes.strings.backDesk, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + }, + actions = { + TopAppBarMoreActions( + items = SettingsTopBarActions.values(), + onItemClick = { + when (it) { + SettingsTopBarActions.RESET -> onResetClick() + } + }, + moreIconDescription = SettingsThemeRes.strings.moreDesk, + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + ), + ) +} + +internal enum class SettingsTopBarActions : TopAppBarAction { + RESET { + override val icon: Int @Composable get() = SettingsThemeRes.icons.reset + override val title: String @Composable get() = SettingsThemeRes.strings.resetTitle + override val isAlwaysShow: Boolean get() = false + }, +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/ThemeChooserDialog.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/ThemeChooserDialog.kt new file mode 100644 index 0000000..6b194a3 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/settings/views/ThemeChooserDialog.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.features.settings.impl.presentation.settings.views + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ru.aleshin.core.ui.theme.material.ThemeUiType +import ru.aleshin.core.ui.views.DialogButtons +import ru.aleshin.features.settings.impl.presentation.mappers.mapToMessage +import ru.aleshin.features.settings.impl.presentation.theme.SettingsThemeRes + +/** + * @author Stanislav Aleshin on 12.07.2023. + */ +@Composable +@OptIn(ExperimentalMaterial3Api::class) +internal fun ThemeChooserDialog( + modifier: Modifier = Modifier, + initTheme: ThemeUiType, + onCloseDialog: () -> Unit, + onChooseTheme: (ThemeUiType) -> Unit, +) { + var currentTheme by remember { mutableStateOf(initTheme) } + + AlertDialog(onDismissRequest = onCloseDialog) { + Surface( + modifier = modifier + .width(280.dp) + .wrapContentHeight(), + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + Column { + Box(modifier = Modifier.padding(24.dp)) { + Text( + text = SettingsThemeRes.strings.themeDialogTitle, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.headlineSmall, + ) + } + LazyColumn(modifier = Modifier.height(300.dp)) { + items(ThemeUiType.values()) { theme -> + SettingsDialogItem( + modifier = Modifier.fillMaxWidth(), + selected = theme == currentTheme, + title = theme.mapToMessage(), + onSelectChange = { currentTheme = theme }, + ) + } + } + DialogButtons( + onCancelClick = onCloseDialog, + onConfirmClick = { onChooseTheme(currentTheme) }, + ) + } + } + } +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/SettingsTheme.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/SettingsTheme.kt new file mode 100644 index 0000000..cfd91b3 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/SettingsTheme.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.settings.impl.presentation.theme + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import ru.aleshin.core.ui.theme.MixPlayerRes +import ru.aleshin.core.ui.theme.tokens.MixPlayerLanguage +import ru.aleshin.features.settings.impl.presentation.theme.tokens.LocalSettingsIcons +import ru.aleshin.features.settings.impl.presentation.theme.tokens.LocalSettingsStrings +import ru.aleshin.features.settings.impl.presentation.theme.tokens.SettingsIcons +import ru.aleshin.features.settings.impl.presentation.theme.tokens.SettingsStrings + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +@Composable +internal fun SettingsTheme(content: @Composable () -> Unit) { + val icons = SettingsIcons.DEFAULT + val strings = when (MixPlayerRes.language) { + MixPlayerLanguage.EN -> SettingsStrings.ENGLISH + MixPlayerLanguage.RU -> SettingsStrings.RUSSIAN + } + + CompositionLocalProvider( + LocalSettingsIcons provides icons, + LocalSettingsStrings provides strings, + content = content, + ) +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/SettingsThemeRes.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/SettingsThemeRes.kt new file mode 100644 index 0000000..d7c9d74 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/SettingsThemeRes.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.settings.impl.presentation.theme + +import androidx.compose.runtime.Composable +import ru.aleshin.features.settings.impl.presentation.theme.tokens.LocalSettingsIcons +import ru.aleshin.features.settings.impl.presentation.theme.tokens.LocalSettingsStrings +import ru.aleshin.features.settings.impl.presentation.theme.tokens.SettingsIcons +import ru.aleshin.features.settings.impl.presentation.theme.tokens.SettingsStrings + +/** + * @author Stanislav Aleshin on 14.06.2023 + */ +internal object SettingsThemeRes { + + val icons: SettingsIcons + @Composable get() = LocalSettingsIcons.current + + val strings: SettingsStrings + @Composable get() = LocalSettingsStrings.current +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/tokens/SettingsIcons.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/tokens/SettingsIcons.kt new file mode 100644 index 0000000..ae1b2b0 --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/tokens/SettingsIcons.kt @@ -0,0 +1,41 @@ +/* + * 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.settings.impl.presentation.theme.tokens + +import androidx.compose.runtime.staticCompositionLocalOf +import ru.aleshin.features.settings.impl.R + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +internal data class SettingsIcons( + val palette: Int, + val reset: Int, + val language: Int, +) { + companion object { + val DEFAULT = SettingsIcons( + palette = R.drawable.ic_palette, + reset = R.drawable.ic_reset, + language = R.drawable.ic_language, + ) + } +} + +internal val LocalSettingsIcons = staticCompositionLocalOf { + error("Settings Icons is not provided") +} diff --git a/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/tokens/SettingsStrings.kt b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/tokens/SettingsStrings.kt new file mode 100644 index 0000000..dc79f1e --- /dev/null +++ b/features/settings/impl/src/main/java/ru/aleshin/features/settings/impl/presentation/theme/tokens/SettingsStrings.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.features.settings.impl.presentation.theme.tokens + +import androidx.compose.runtime.staticCompositionLocalOf + +/** + * @author Stanislav Aleshin on 14.06.2023. + */ +internal data class SettingsStrings( + val settingsHeader: String, + val generalSettingsHeader: String, + val themeTitle: String, + val appLanguageTitle: String, + val otherError: String, + val backDesk: String, + val moreDesk: String, + val resetTitle: String, + val defaultTitle: String, + val lightTheme: String, + val darkTheme: String, + val russianLanguage: String, + val englishLanguage: String, + val themeDialogTitle: String, +) { + companion object { + val RUSSIAN = SettingsStrings( + settingsHeader = "Настройки", + generalSettingsHeader = "Основные", + themeTitle = "Тема", + appLanguageTitle = "Язык приложения", + otherError = "Ошибка! Обратитесь к разработчику!", + backDesk = "Назад", + moreDesk = "Открыть меню", + resetTitle = "По умолчанию", + defaultTitle = "По умолчанию", + lightTheme = "Светлая", + darkTheme = "Тёмная", + russianLanguage = "Русский", + englishLanguage = "English", + themeDialogTitle = "Тема приложения", + ) + val ENGLISH = SettingsStrings( + settingsHeader = "Settings", + generalSettingsHeader = "General", + themeTitle = "Theme", + appLanguageTitle = "App language", + otherError = "Error! Contact the developer!", + backDesk = "Назад", + moreDesk = "Открыть меню", + resetTitle = "By default", + defaultTitle = "Default", + lightTheme = "Light", + darkTheme = "Dark", + russianLanguage = "Русский", + englishLanguage = "English", + themeDialogTitle = "App theme", + ) + } +} + +internal val LocalSettingsStrings = staticCompositionLocalOf { + error("Settings Strings is not provided") +} diff --git a/features/settings/impl/src/main/res/drawable/ic_categories.xml b/features/settings/impl/src/main/res/drawable/ic_categories.xml new file mode 100644 index 0000000..d092ef6 --- /dev/null +++ b/features/settings/impl/src/main/res/drawable/ic_categories.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/settings/impl/src/main/res/drawable/ic_language.xml b/features/settings/impl/src/main/res/drawable/ic_language.xml new file mode 100644 index 0000000..a5061ae --- /dev/null +++ b/features/settings/impl/src/main/res/drawable/ic_language.xml @@ -0,0 +1,5 @@ + + + diff --git a/features/settings/impl/src/main/res/drawable/ic_palette.xml b/features/settings/impl/src/main/res/drawable/ic_palette.xml new file mode 100644 index 0000000..d6314cc --- /dev/null +++ b/features/settings/impl/src/main/res/drawable/ic_palette.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/settings/impl/src/main/res/drawable/ic_reset.xml b/features/settings/impl/src/main/res/drawable/ic_reset.xml new file mode 100644 index 0000000..ad7bf63 --- /dev/null +++ b/features/settings/impl/src/main/res/drawable/ic_reset.xml @@ -0,0 +1,5 @@ + + + diff --git a/features/settings/impl/src/test/java/ru/aleshin/features/settings/impl/ExampleUnitTest.kt b/features/settings/impl/src/test/java/ru/aleshin/features/settings/impl/ExampleUnitTest.kt new file mode 100644 index 0000000..e7ad9df --- /dev/null +++ b/features/settings/impl/src/test/java/ru/aleshin/features/settings/impl/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.settings.impl + +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/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3c5031e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..ca1e9b8 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,33 @@ +[versions] +agp = "8.2.0-alpha10" +kotlin = "1.8.10" +core-ktx = "1.9.0" +junit = "4.13.2" +androidx-test-ext-junit = "1.1.5" +espresso-core = "3.5.1" +lifecycle-runtime-ktx = "2.6.1" +activity-compose = "1.7.0" +compose-bom = "2023.03.00" + +[libraries] +core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } +espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } +lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" } +activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } +compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } +ui = { group = "androidx.compose.ui", name = "ui" } +ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +material3 = { group = "androidx.compose.material3", name = "material3" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + +[bundles] + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f790166 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Jul 12 13:11:26 MSK 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/module_injector/.gitignore b/module_injector/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/module_injector/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/module_injector/build.gradle.kts b/module_injector/build.gradle.kts new file mode 100644 index 0000000..c6cfa23 --- /dev/null +++ b/module_injector/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +repositories { + mavenCentral() + google() +} + +android { + namespace = "ru.aleshin.module_injector" + compileSdk = Config.compileSdkVersion + + defaultConfig { + minSdk = Config.minSdkVersion + + testInstrumentationRunner = Config.testInstrumentRunner + consumerProguardFiles("consumer-rules.pro") + } + + 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 + } +} + +dependencies { + implementation(Dependencies.AndroidX.core) + testImplementation(Dependencies.Test.jUnit) + androidTestImplementation(Dependencies.Test.jUnitExt) + androidTestImplementation(Dependencies.Test.espresso) +} diff --git a/module_injector/consumer-rules.pro b/module_injector/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/module_injector/proguard-rules.pro b/module_injector/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/module_injector/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/module_injector/src/androidTest/java/ru/aleshin/module_injector/ExampleInstrumentedTest.kt b/module_injector/src/androidTest/java/ru/aleshin/module_injector/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d4cfbda --- /dev/null +++ b/module_injector/src/androidTest/java/ru/aleshin/module_injector/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.module_injector + +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.module_injector.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/module_injector/src/main/AndroidManifest.xml b/module_injector/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c193aa3 --- /dev/null +++ b/module_injector/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/module_injector/src/main/java/ru/aleshin/module_injector/BaseComponentHolder.kt b/module_injector/src/main/java/ru/aleshin/module_injector/BaseComponentHolder.kt new file mode 100644 index 0000000..9d225af --- /dev/null +++ b/module_injector/src/main/java/ru/aleshin/module_injector/BaseComponentHolder.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.module_injector + +/** + * @author Stanislav Aleshin on 12.06.2023. + */ +interface BaseComponentHolder { + + /** + * Initializes the internal DI graph to be able to get the API features. + * + * @param dependencies needed for the features to work + */ + fun init(dependencies: D) + + /** + * Allows get API for this features if DI graph is initialize. + * + * @return [A] API for working with features + * + * @exception IllegalStateException if DI graph is not initialized. + */ + fun fetchApi(): A + + /** + * Deleting the internal DI graph to close feature. + */ + fun clear() +} diff --git a/module_injector/src/main/java/ru/aleshin/module_injector/BaseFeatureApi.kt b/module_injector/src/main/java/ru/aleshin/module_injector/BaseFeatureApi.kt new file mode 100644 index 0000000..fc16542 --- /dev/null +++ b/module_injector/src/main/java/ru/aleshin/module_injector/BaseFeatureApi.kt @@ -0,0 +1,22 @@ +/* + * 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.module_injector + +/** + * @author Stanislav Aleshin on 12.06.2023. + */ +interface BaseFeatureApi + diff --git a/module_injector/src/main/java/ru/aleshin/module_injector/BaseFeatureDependencies.kt b/module_injector/src/main/java/ru/aleshin/module_injector/BaseFeatureDependencies.kt new file mode 100644 index 0000000..38c1cf8 --- /dev/null +++ b/module_injector/src/main/java/ru/aleshin/module_injector/BaseFeatureDependencies.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 + * imitations under the License. + */ +package ru.aleshin.module_injector + +/** + * @author Stanislav Aleshin on 12.06.2023. + */ +interface BaseFeatureDependencies diff --git a/module_injector/src/test/java/ru/aleshin/module_injector/ExampleUnitTest.kt b/module_injector/src/test/java/ru/aleshin/module_injector/ExampleUnitTest.kt new file mode 100644 index 0000000..55f905a --- /dev/null +++ b/module_injector/src/test/java/ru/aleshin/module_injector/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.module_injector + +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/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..39721c9 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "MixPlayer" + +include(":app") +include(":module_injector") +include(":core:common") +include(":core:ui") +include(":features:settings:impl") +include(":features:settings:api") +include(":features:home:impl") +include(":features:home:api") +include(":features:player:impl") +include(":features:player:api")