diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index bd81a117f..455db8fad 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,6 +1,6 @@
+import java.io.FileInputStream
import org.apache.commons.io.output.ByteArrayOutputStream
import org.jetbrains.kotlin.konan.properties.Properties
-import java.io.FileInputStream
@Suppress("dsl_scope_violation")
plugins {
@@ -11,7 +11,6 @@ plugins {
alias(libs.plugins.firebase.appdistribution)
alias(libs.plugins.firebase.crashlytics)
alias(libs.plugins.palantir.git)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -91,6 +90,7 @@ android {
isShrinkResources = true
signingConfig = signingConfigs.getByName("release")
applicationIdSuffix = MoviesBuildType.RELEASE.applicationIdSuffix
+ manifestPlaceholders += mapOf("appName" to "@string/app_name")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
@@ -98,7 +98,6 @@ android {
"okhttp3.pro",
"coroutines.pro"
)
-
firebaseAppDistribution {
appId = "1:770317857182:android:876190afbc53df31"
artifactType = "APK"
@@ -113,6 +112,7 @@ android {
isMinifyEnabled = false
isShrinkResources = false
applicationIdSuffix = MoviesBuildType.DEBUG.applicationIdSuffix
+ manifestPlaceholders += mapOf("appName" to "@string/app_name_dev")
}
create("benchmark") {
initWith(getByName("release"))
@@ -153,11 +153,11 @@ android {
dependencies {
implementation(project(":core:analytics"))
implementation(project(":core:common"))
- implementation(project(":core:domain"))
+ implementation(project(":core:interactor"))
implementation(project(":core:navigation"))
implementation(project(":core:notifications"))
implementation(project(":core:ui"))
-
+ implementation(project(":core:work"))
implementation(project(":feature:auth"))
implementation(project(":feature:account"))
implementation(project(":feature:details"))
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
deleted file mode 100644
index f7c3d4aaf..000000000
--- a/app/src/debug/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- Movies Dev
-
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cc7fa80c8..f6673d20c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,7 +2,8 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ android:installLocation="auto">
@@ -23,12 +24,13 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="true"
android:hardwareAccelerated="true"
- android:icon="@mipmap/ic_launcher"
- android:label="@string/app_name"
+ android:label="${appName}"
android:localeConfig="@xml/locale_config"
- android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
- android:theme="@style/Theme.Movies.Starting">
+ android:theme="@style/Theme.Movies.Starting"
+ android:icon="@mipmap/ic_launcher_red"
+ android:roundIcon="@mipmap/ic_launcher_red"
+ android:enableOnBackInvokedCallback="true">
+ android:screenOrientation="fullUser">
@@ -89,11 +91,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/kotlin/org/michaelbel/movies/App.kt b/app/src/main/kotlin/org/michaelbel/movies/App.kt
index b39882247..b3c0da061 100644
--- a/app/src/main/kotlin/org/michaelbel/movies/App.kt
+++ b/app/src/main/kotlin/org/michaelbel/movies/App.kt
@@ -1,28 +1,24 @@
package org.michaelbel.movies
import android.app.Application
-import android.util.Log
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.google.firebase.FirebaseApp
import dagger.hilt.android.HiltAndroidApp
-import org.michaelbel.moviemade.BuildConfig
import javax.inject.Inject
+import org.michaelbel.movies.ui.appicon.installLauncherIcon
@HiltAndroidApp
internal class App: Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory
- override fun getWorkManagerConfiguration(): Configuration {
- return Configuration.Builder()
- .setWorkerFactory(workerFactory)
- .setMinimumLoggingLevel(if (BuildConfig.DEBUG) Log.DEBUG else Log.ERROR)
- .build()
- }
+ override val workManagerConfiguration: Configuration
+ get() = Configuration.Builder().setWorkerFactory(workerFactory).build()
override fun onCreate() {
super.onCreate()
+ installLauncherIcon()
FirebaseApp.initializeApp(this)
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/michaelbel/movies/MainActivity.kt b/app/src/main/kotlin/org/michaelbel/movies/MainActivity.kt
index 6e6ab0ce0..4cf40eb42 100644
--- a/app/src/main/kotlin/org/michaelbel/movies/MainActivity.kt
+++ b/app/src/main/kotlin/org/michaelbel/movies/MainActivity.kt
@@ -2,19 +2,12 @@ package org.michaelbel.movies
import android.os.Bundle
import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.core.view.WindowCompat
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint
-import org.michaelbel.movies.common.theme.AppTheme
-import org.michaelbel.movies.navigation.ktx.addOnDestinationChangedListener
import org.michaelbel.movies.ui.shortcuts.installShortcuts
-import org.michaelbel.movies.ui.theme.MoviesTheme
/**
* Per-App Language depends on AppCompatActivity (not ComponentActivity).
@@ -26,29 +19,13 @@ internal class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
-
- /** Configure edge-to-edge display. */
- WindowCompat.setDecorFitsSystemWindows(window, false)
-
installShortcuts()
setContent {
- val currentTheme: AppTheme by viewModel.currentTheme.collectAsStateWithLifecycle()
- val dynamicColors: Boolean by viewModel.dynamicColors.collectAsStateWithLifecycle()
-
- val navHostController: NavHostController = rememberNavController().apply {
- addOnDestinationChangedListener(viewModel::analyticsTrackDestination)
- }
-
- MoviesTheme(
- theme = currentTheme,
- dynamicColors = dynamicColors
- ) {
- MainActivityContent(
- navHostController = navHostController,
- onStartUpdateFlow = { viewModel.startUpdateFlow(this) }
- )
- }
+ MainActivityContent(
+ onStartUpdateFlow = { viewModel.startUpdateFlow(this) }
+ )
}
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/michaelbel/movies/MainActivityContent.kt b/app/src/main/kotlin/org/michaelbel/movies/MainActivityContent.kt
index 55b561977..7a8b9717c 100644
--- a/app/src/main/kotlin/org/michaelbel/movies/MainActivityContent.kt
+++ b/app/src/main/kotlin/org/michaelbel/movies/MainActivityContent.kt
@@ -1,6 +1,9 @@
package org.michaelbel.movies
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
@@ -8,47 +11,60 @@ import org.michaelbel.movies.auth.accountGraph
import org.michaelbel.movies.auth.authGraph
import org.michaelbel.movies.auth.navigateToAccount
import org.michaelbel.movies.auth.navigateToAuth
+import org.michaelbel.movies.common.theme.AppTheme
import org.michaelbel.movies.details.detailsGraph
import org.michaelbel.movies.details.navigateToDetails
import org.michaelbel.movies.feed.FeedDestination
import org.michaelbel.movies.feed.feedGraph
import org.michaelbel.movies.gallery.galleryGraph
import org.michaelbel.movies.gallery.navigateToGallery
+import org.michaelbel.movies.navigation.ktx.addOnDestinationChangedListener
import org.michaelbel.movies.settings.navigateToSettings
import org.michaelbel.movies.settings.settingsGraph
+import org.michaelbel.movies.ui.theme.MoviesTheme
@Composable
internal fun MainActivityContent(
- navHostController: NavHostController = rememberNavController(),
- startDestination: String = FeedDestination.route,
- onStartUpdateFlow: () -> Unit
+ onStartUpdateFlow: () -> Unit,
+ viewModel: MainViewModel = hiltViewModel()
) {
- NavHost(
- navController = navHostController,
- startDestination = startDestination
+ val currentTheme: AppTheme by viewModel.currentTheme.collectAsStateWithLifecycle()
+ val dynamicColors: Boolean by viewModel.dynamicColors.collectAsStateWithLifecycle()
+ val navHostController: NavHostController = rememberNavController().apply {
+ addOnDestinationChangedListener(viewModel::analyticsTrackDestination)
+ }
+
+ MoviesTheme(
+ theme = currentTheme,
+ dynamicColors = dynamicColors
) {
- authGraph(
- navigateBack = navHostController::popBackStack
- )
- accountGraph(
- navigateBack = navHostController::popBackStack
- )
- feedGraph(
- navigateToAuth = navHostController::navigateToAuth,
- navigateToAccount = navHostController::navigateToAccount,
- navigateToSettings = navHostController::navigateToSettings,
- navigateToDetails = navHostController::navigateToDetails,
- onStartUpdateFlow = onStartUpdateFlow
- )
- detailsGraph(
- navigateBack = navHostController::popBackStack,
- navigateToGallery = navHostController::navigateToGallery
- )
- galleryGraph(
- navigateBack = navHostController::popBackStack
- )
- settingsGraph(
- navigateBack = navHostController::popBackStack
- )
+ NavHost(
+ navController = navHostController,
+ startDestination = FeedDestination.route
+ ) {
+ authGraph(
+ navigateBack = navHostController::popBackStack
+ )
+ accountGraph(
+ navigateBack = navHostController::popBackStack
+ )
+ feedGraph(
+ navigateToAuth = navHostController::navigateToAuth,
+ navigateToAccount = navHostController::navigateToAccount,
+ navigateToSettings = navHostController::navigateToSettings,
+ navigateToDetails = navHostController::navigateToDetails,
+ onStartUpdateFlow = onStartUpdateFlow
+ )
+ detailsGraph(
+ navigateBack = navHostController::popBackStack,
+ navigateToGallery = navHostController::navigateToGallery
+ )
+ galleryGraph(
+ navigateBack = navHostController::popBackStack
+ )
+ settingsGraph(
+ navigateBack = navHostController::popBackStack
+ )
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/michaelbel/movies/MainViewModel.kt b/app/src/main/kotlin/org/michaelbel/movies/MainViewModel.kt
index ed107ce1a..6e9932d62 100644
--- a/app/src/main/kotlin/org/michaelbel/movies/MainViewModel.kt
+++ b/app/src/main/kotlin/org/michaelbel/movies/MainViewModel.kt
@@ -8,6 +8,7 @@ import androidx.work.WorkManager
import androidx.work.workDataOf
import com.google.firebase.messaging.FirebaseMessaging
import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
@@ -17,18 +18,17 @@ import org.michaelbel.movies.common.inappupdate.di.InAppUpdate
import org.michaelbel.movies.common.ktx.printlnDebug
import org.michaelbel.movies.common.theme.AppTheme
import org.michaelbel.movies.common.viewmodel.BaseViewModel
-import org.michaelbel.movies.domain.workers.AccountUpdateWorker
-import org.michaelbel.movies.domain.workers.MoviesDatabaseWorker
import org.michaelbel.movies.interactor.Interactor
-import javax.inject.Inject
+import org.michaelbel.movies.work.AccountUpdateWorker
+import org.michaelbel.movies.work.MoviesDatabaseWorker
@HiltViewModel
internal class MainViewModel @Inject constructor(
private val interactor: Interactor,
private val inAppUpdate: InAppUpdate,
- private val workManager: WorkManager,
private val analytics: MoviesAnalytics,
- private val firebaseMessaging: FirebaseMessaging
+ private val firebaseMessaging: FirebaseMessaging,
+ private val workManager: WorkManager
): BaseViewModel() {
val currentTheme: StateFlow = interactor.currentTheme
@@ -84,7 +84,7 @@ internal class MainViewModel @Inject constructor(
workManager.enqueue(request)
}
- private companion object {
- private const val MOVIES_DATA_FILENAME = "movies.json"
+ companion object {
+ const val MOVIES_DATA_FILENAME = "movies.json"
}
}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f1b2097d2..0894e3731 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
Movies
+ Movies Dev
\ No newline at end of file
diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts
index fa9a2ede9..53e0a193e 100644
--- a/benchmark/build.gradle.kts
+++ b/benchmark/build.gradle.kts
@@ -2,21 +2,14 @@ plugins {
id("com.android.test")
id("org.jetbrains.kotlin.android")
id("kotlin-android")
- alias(libs.plugins.detekt)
}
android {
namespace = "org.michaelbel.movies.benchmark"
- compileSdk = libs.versions.compile.sdk.get().toInt()
-
- kotlinOptions {
- freeCompilerArgs = freeCompilerArgs + listOf(
- "-opt-in=androidx.benchmark.macro.ExperimentalBaselineProfilesApi"
- )
- }
defaultConfig {
- minSdk = libs.versions.benchmark.min.sdk.get().toInt()
+ minSdk = libs.versions.min.sdk.get().toInt()
+ compileSdk = libs.versions.compile.sdk.get().toInt()
targetSdk = libs.versions.target.sdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -29,12 +22,19 @@ android {
}
}
+ kotlinOptions {
+ freeCompilerArgs = freeCompilerArgs + listOf(
+ "-opt-in=androidx.benchmark.macro.ExperimentalBaselineProfilesApi"
+ )
+ }
+
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
targetProjectPath = ":app"
+
experimentalProperties["android.experimental.self-instrumenting"] = true
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 2ab15ee04..609779c24 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -18,4 +18,8 @@ plugins {
detekt {
config.setFrom("$projectDir/config/detekt/detekt.yml")
+}
+
+subprojects {
+ apply(plugin = "io.gitlab.arturbosch.detekt")
}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/ktx/DependencyHandlerScope.kt b/buildSrc/src/main/kotlin/ktx/DependencyHandlerScope.kt
index d7018541b..34e86b3f0 100644
--- a/buildSrc/src/main/kotlin/ktx/DependencyHandlerScope.kt
+++ b/buildSrc/src/main/kotlin/ktx/DependencyHandlerScope.kt
@@ -10,8 +10,8 @@ internal fun DependencyHandlerScope.implementation(
"implementation"(dependency)
}
-internal fun DependencyHandlerScope.kapt(
+internal fun DependencyHandlerScope.ksp(
dependency: Provider
) {
- "kapt"(dependency)
+ "ksp"(dependency)
}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/plugins/AndroidHiltConventionPlugin.kt b/buildSrc/src/main/kotlin/plugins/AndroidHiltConventionPlugin.kt
index 88a9d9016..9c881017a 100644
--- a/buildSrc/src/main/kotlin/plugins/AndroidHiltConventionPlugin.kt
+++ b/buildSrc/src/main/kotlin/plugins/AndroidHiltConventionPlugin.kt
@@ -1,7 +1,7 @@
package plugins
import ktx.implementation
-import ktx.kapt
+import ktx.ksp
import ktx.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -13,12 +13,12 @@ internal class AndroidHiltConventionPlugin: Plugin {
target.run {
pluginManager.run {
apply("dagger.hilt.android.plugin")
- apply("org.jetbrains.kotlin.kapt")
+ apply("com.google.devtools.ksp")
}
dependencies {
implementation(libs.findLibrary("hilt.android").get())
- kapt(libs.findLibrary("hilt.compiler").get())
+ ksp(libs.findLibrary("hilt.compiler").get())
}
}
}
diff --git a/config/images/10.png b/config/images/10.png
new file mode 100644
index 000000000..8dfb16434
Binary files /dev/null and b/config/images/10.png differ
diff --git a/config/images/4.png b/config/images/4.png
index 79e1dbe87..8735fce1a 100644
Binary files a/config/images/4.png and b/config/images/4.png differ
diff --git a/config/images/5.png b/config/images/5.png
index 0053343fb..8970dbd70 100644
Binary files a/config/images/5.png and b/config/images/5.png differ
diff --git a/config/images/9.png b/config/images/9.png
new file mode 100644
index 000000000..9f2bf3eef
Binary files /dev/null and b/config/images/9.png differ
diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts
index 249203833..0f8374f9e 100644
--- a/core/analytics/build.gradle.kts
+++ b/core/analytics/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts
index a04e86799..423540a9f 100644
--- a/core/common/build.gradle.kts
+++ b/core/common/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -47,7 +46,7 @@ android {
dependencies {
implementation(project(":core:analytics"))
- api(project(":core:entities"))
+ implementation(project(":core:network"))
api(libs.bundles.kotlin.coroutines)
api(libs.firebase.config.ktx)
api(libs.gms.play.services.base)
@@ -55,11 +54,14 @@ dependencies {
api(libs.androidx.activity.compose)
api(libs.androidx.core.ktx)
api(libs.androidx.paging.compose)
+ api(libs.androidx.work.runtime.ktx)
+ api(libs.androidx.hilt.work)
api(libs.bundles.lifecycle)
api(libs.timber)
implementation(libs.bundles.appcompat)
implementation(libs.firebase.crashlytics.ktx)
implementation(libs.androidx.startup.runtime)
implementation(libs.androidx.browser)
+
lintChecks(libs.lint.checks)
}
\ No newline at end of file
diff --git a/core/common/src/main/AndroidManifest.xml b/core/common/src/main/AndroidManifest.xml
index 07a9a6fc2..ba0156dbe 100644
--- a/core/common/src/main/AndroidManifest.xml
+++ b/core/common/src/main/AndroidManifest.xml
@@ -11,6 +11,11 @@
android:exported="false"
tools:node="merge">
+
+
diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/inappupdate/di/InAppUpdate.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/inappupdate/di/InAppUpdate.kt
index a3d52ce5a..984813d2f 100644
--- a/core/common/src/main/kotlin/org/michaelbel/movies/common/inappupdate/di/InAppUpdate.kt
+++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/inappupdate/di/InAppUpdate.kt
@@ -4,12 +4,14 @@ import android.app.Activity
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateOptions
+import com.google.android.play.core.install.InstallException
import com.google.android.play.core.install.model.AppUpdateType
+import com.google.android.play.core.install.model.InstallErrorCode
import com.google.android.play.core.install.model.UpdateAvailability
import com.google.android.play.core.tasks.Task
+import javax.inject.Inject
import org.michaelbel.movies.common.googleapi.GoogleApi
import timber.log.Timber
-import javax.inject.Inject
class InAppUpdate @Inject constructor(
private val appUpdateManager: AppUpdateManager,
@@ -43,6 +45,7 @@ class InAppUpdate @Inject constructor(
}
private fun onFailureAppUpdate(throwable: Throwable) {
+ if (throwable is InstallException && throwable.errorCode == InstallErrorCode.ERROR_APP_NOT_OWNED) return
Timber.e(throwable)
}
}
\ No newline at end of file
diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/TimeKtx.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/TimeKtx.kt
index 5392a7827..f46384896 100644
--- a/core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/TimeKtx.kt
+++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/TimeKtx.kt
@@ -1,5 +1,17 @@
package org.michaelbel.movies.common.ktx
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+private const val DATE_TIME_FORMAT = "yyyyMMdd_HHmmss"
+
+val currentDateTime: String
+ get() {
+ val simpleDateFormat = SimpleDateFormat(DATE_TIME_FORMAT, Locale.getDefault())
+ return simpleDateFormat.format(Date())
+ }
+
fun isTimePasses(interval: Long, expireTime: Long, currentTime: Long): Boolean {
return expireTime == 0L || currentTime.minus(expireTime) >= interval
}
\ No newline at end of file
diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/usecase/UseCase.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/usecase/UseCase.kt
index c81f74d79..1e37f4959 100644
--- a/core/common/src/main/kotlin/org/michaelbel/movies/common/usecase/UseCase.kt
+++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/usecase/UseCase.kt
@@ -2,7 +2,7 @@ package org.michaelbel.movies.common.usecase
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
-import org.michaelbel.movies.entities.Either
+import org.michaelbel.movies.network.Either
import timber.log.Timber
abstract class UseCase(
diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/version/AppVersionData.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/version/AppVersionData.kt
index f2061d6cd..16af2550f 100644
--- a/core/common/src/main/kotlin/org/michaelbel/movies/common/version/AppVersionData.kt
+++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/version/AppVersionData.kt
@@ -6,7 +6,7 @@ data class AppVersionData(
val isDebug: Boolean
) {
companion object {
- val None: AppVersionData = AppVersionData(
+ val Empty: AppVersionData = AppVersionData(
version = "",
code = 0L,
isDebug = false
diff --git a/core/entities/.gitignore b/core/entities/.gitignore
deleted file mode 100644
index 42afabfd2..000000000
--- a/core/entities/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/core/entities/build.gradle.kts b/core/entities/build.gradle.kts
deleted file mode 100644
index e6f270f0c..000000000
--- a/core/entities/build.gradle.kts
+++ /dev/null
@@ -1,50 +0,0 @@
-import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
-
-@Suppress("dsl_scope_violation")
-plugins {
- alias(libs.plugins.library)
- alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
- id("movies-android-hilt")
-}
-
-val tmdbApiKey: String by lazy {
- gradleLocalProperties(rootDir).getProperty("TMDB_API_KEY").orEmpty().ifEmpty {
- System.getenv("TMDB_API_KEY").orEmpty()
- }
-}
-
-android {
- namespace = "org.michaelbel.movies.entities"
-
- defaultConfig {
- minSdk = libs.versions.min.sdk.get().toInt()
- compileSdk = libs.versions.compile.sdk.get().toInt()
- buildConfigField("String", "TMDB_API_KEY", "\"$tmdbApiKey\"")
- }
-
- /*buildTypes {
- create("benchmark") {
- signingConfig = signingConfigs.getByName("debug")
- matchingFallbacks += listOf("release")
- initWith(getByName("release"))
- }
- }*/
-
- buildFeatures {
- buildConfig = true
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
-
- lint {
- quiet = true
- abortOnError = false
- ignoreWarnings = true
- checkDependencies = true
- lintConfig = file("${project.rootDir}/config/codestyle/lint.xml")
- }
-}
\ No newline at end of file
diff --git a/core/entities/src/main/AndroidManifest.xml b/core/entities/src/main/AndroidManifest.xml
deleted file mode 100644
index 1d26c87a1..000000000
--- a/core/entities/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/core/interactor-impl/build.gradle.kts b/core/interactor-impl/build.gradle.kts
index 5342207f9..abecbdf2a 100644
--- a/core/interactor-impl/build.gradle.kts
+++ b/core/interactor-impl/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
diff --git a/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/AccountInteractorImpl.kt b/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/AccountInteractorImpl.kt
index cea323601..24ef59443 100644
--- a/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/AccountInteractorImpl.kt
+++ b/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/AccountInteractorImpl.kt
@@ -1,5 +1,7 @@
package org.michaelbel.movies.interactor
+import javax.inject.Inject
+import javax.inject.Singleton
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
@@ -7,8 +9,6 @@ import org.michaelbel.movies.common.dispatchers.MoviesDispatchers
import org.michaelbel.movies.interactor.usecase.DelayUseCase
import org.michaelbel.movies.persistence.database.entity.AccountDb
import org.michaelbel.movies.repository.AccountRepository
-import javax.inject.Inject
-import javax.inject.Singleton
@Singleton
internal class AccountInteractorImpl @Inject constructor(
@@ -19,9 +19,22 @@ internal class AccountInteractorImpl @Inject constructor(
override val account: Flow = accountRepository.account
- override suspend fun accountDetails() {
- delay(delayUseCase.networkRequestDelay())
+ override suspend fun accountId(): Int? {
+ return withContext(dispatchers.io) {
+ accountRepository.accountId()
+ }
+ }
+
+ override suspend fun accountExpireTime(): Long? {
+ return withContext(dispatchers.io) {
+ accountRepository.accountExpireTime()
+ }
+ }
- return withContext(dispatchers.io) { accountRepository.accountDetails() }
+ override suspend fun accountDetails() {
+ return withContext(dispatchers.io) {
+ delay(delayUseCase.networkRequestDelay())
+ accountRepository.accountDetails()
+ }
}
}
\ No newline at end of file
diff --git a/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/ImageInteractorImpl.kt b/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/ImageInteractorImpl.kt
new file mode 100644
index 000000000..92982c1b6
--- /dev/null
+++ b/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/ImageInteractorImpl.kt
@@ -0,0 +1,26 @@
+package org.michaelbel.movies.interactor
+
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
+import org.michaelbel.movies.common.dispatchers.MoviesDispatchers
+import org.michaelbel.movies.persistence.database.entity.ImageDb
+import org.michaelbel.movies.repository.ImageRepository
+
+@Singleton
+internal class ImageInteractorImpl @Inject constructor(
+ private val dispatchers: MoviesDispatchers,
+ private val imageRepository: ImageRepository
+): ImageInteractor {
+
+ override fun imagesFlow(movieId: Int): Flow> {
+ return imageRepository.imagesFlow(movieId)
+ }
+
+ override suspend fun images(movieId: Int) {
+ return withContext(dispatchers.io) {
+ imageRepository.images(movieId)
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/MovieInteractorImpl.kt b/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/MovieInteractorImpl.kt
index d1772074d..e7526b373 100644
--- a/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/MovieInteractorImpl.kt
+++ b/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/MovieInteractorImpl.kt
@@ -4,12 +4,10 @@ import androidx.paging.PagingSource
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import org.michaelbel.movies.common.dispatchers.MoviesDispatchers
-import org.michaelbel.movies.entities.Either
import org.michaelbel.movies.interactor.usecase.DelayUseCase
-import org.michaelbel.movies.network.model.ImagesResponse
+import org.michaelbel.movies.network.Either
import org.michaelbel.movies.network.model.MovieResponse
import org.michaelbel.movies.network.model.Result
import org.michaelbel.movies.persistence.database.entity.MovieDb
@@ -26,10 +24,6 @@ internal class MovieInteractorImpl @Inject constructor(
return movieRepository.moviesPagingSource(movieList)
}
- override fun movieImage(movieId: Int): Flow {
- return movieRepository.movieImage(movieId)
- }
-
override suspend fun moviesResult(movieList: String, page: Int): Result {
delay(delayUseCase.networkRequestDelay())
@@ -46,12 +40,6 @@ internal class MovieInteractorImpl @Inject constructor(
}
}
- override suspend fun movieImages(movieId: Int): ImagesResponse {
- return withContext(dispatchers.io) {
- movieRepository.movieImages(movieId)
- }
- }
-
override suspend fun removeAllMovies(movieList: String) {
return withContext(dispatchers.io) {
movieRepository.removeAllMovies(movieList)
diff --git a/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/NotificationInteractorImpl.kt b/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/NotificationInteractorImpl.kt
new file mode 100644
index 000000000..7ec2c1cb7
--- /dev/null
+++ b/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/NotificationInteractorImpl.kt
@@ -0,0 +1,26 @@
+package org.michaelbel.movies.interactor
+
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.withContext
+import org.michaelbel.movies.common.dispatchers.MoviesDispatchers
+import org.michaelbel.movies.repository.NotificationRepository
+
+@Singleton
+internal class NotificationInteractorImpl @Inject constructor(
+ private val dispatchers: MoviesDispatchers,
+ private val notificationRepository: NotificationRepository
+): NotificationInteractor {
+
+ override suspend fun notificationExpireTime(): Long {
+ return withContext(dispatchers.io) {
+ notificationRepository.notificationExpireTime()
+ }
+ }
+
+ override suspend fun updateNotificationExpireTime() {
+ withContext(dispatchers.io) {
+ notificationRepository.updateNotificationExpireTime()
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/di/InteractorModule.kt b/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/di/InteractorModule.kt
index bae511499..9832e8bd2 100644
--- a/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/di/InteractorModule.kt
+++ b/core/interactor-impl/src/main/kotlin/org/michaelbel/movies/interactor/di/InteractorModule.kt
@@ -4,15 +4,19 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
import org.michaelbel.movies.interactor.AccountInteractor
import org.michaelbel.movies.interactor.AccountInteractorImpl
import org.michaelbel.movies.interactor.AuthenticationInteractor
import org.michaelbel.movies.interactor.AuthenticationInteractorImpl
+import org.michaelbel.movies.interactor.ImageInteractor
+import org.michaelbel.movies.interactor.ImageInteractorImpl
import org.michaelbel.movies.interactor.MovieInteractor
import org.michaelbel.movies.interactor.MovieInteractorImpl
+import org.michaelbel.movies.interactor.NotificationInteractor
+import org.michaelbel.movies.interactor.NotificationInteractorImpl
import org.michaelbel.movies.interactor.SettingsInteractor
import org.michaelbel.movies.interactor.SettingsInteractorImpl
-import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@@ -20,9 +24,9 @@ internal interface InteractorModule {
@Binds
@Singleton
- fun provideMovieInteractor(
- interactor: MovieInteractorImpl
- ): MovieInteractor
+ fun provideAccountInteractor(
+ interactor: AccountInteractorImpl
+ ): AccountInteractor
@Binds
@Singleton
@@ -32,9 +36,21 @@ internal interface InteractorModule {
@Binds
@Singleton
- fun provideAccountInteractor(
- interactor: AccountInteractorImpl
- ): AccountInteractor
+ fun provideImageInteractor(
+ interactor: ImageInteractorImpl
+ ): ImageInteractor
+
+ @Binds
+ @Singleton
+ fun provideMovieInteractor(
+ interactor: MovieInteractorImpl
+ ): MovieInteractor
+
+ @Binds
+ @Singleton
+ fun provideNotificationInteractor(
+ interactor: NotificationInteractorImpl
+ ): NotificationInteractor
@Binds
@Singleton
diff --git a/core/interactor/build.gradle.kts b/core/interactor/build.gradle.kts
index e831f8927..8c5e16371 100644
--- a/core/interactor/build.gradle.kts
+++ b/core/interactor/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -37,10 +36,9 @@ android {
}
dependencies {
+ implementation(project(":core:network"))
api(project(":core:analytics"))
api(project(":core:common"))
- api(project(":core:network"))
api(project(":core:persistence"))
api(project(":core:repository"))
- implementation(project(":core:entities"))
}
\ No newline at end of file
diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/AccountInteractor.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/AccountInteractor.kt
index 9ad1723a1..8932c6227 100644
--- a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/AccountInteractor.kt
+++ b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/AccountInteractor.kt
@@ -7,5 +7,9 @@ interface AccountInteractor {
val account: Flow
+ suspend fun accountId(): Int?
+
+ suspend fun accountExpireTime(): Long?
+
suspend fun accountDetails()
}
\ No newline at end of file
diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/ImageInteractor.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/ImageInteractor.kt
new file mode 100644
index 000000000..104a26771
--- /dev/null
+++ b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/ImageInteractor.kt
@@ -0,0 +1,11 @@
+package org.michaelbel.movies.interactor
+
+import kotlinx.coroutines.flow.Flow
+import org.michaelbel.movies.persistence.database.entity.ImageDb
+
+interface ImageInteractor {
+
+ fun imagesFlow(movieId: Int): Flow>
+
+ suspend fun images(movieId: Int)
+}
\ No newline at end of file
diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/Interactor.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/Interactor.kt
index 12d5b864c..4c6119265 100644
--- a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/Interactor.kt
+++ b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/Interactor.kt
@@ -5,9 +5,13 @@ import javax.inject.Inject
class Interactor @Inject constructor(
accountInteractor: AccountInteractor,
authenticationInteractor: AuthenticationInteractor,
+ imageInteractor: ImageInteractor,
movieInteractor: MovieInteractor,
+ notificationInteractor: NotificationInteractor,
settingsInteractor: SettingsInteractor
): AccountInteractor by accountInteractor,
AuthenticationInteractor by authenticationInteractor,
+ ImageInteractor by imageInteractor,
MovieInteractor by movieInteractor,
+ NotificationInteractor by notificationInteractor,
SettingsInteractor by settingsInteractor
\ No newline at end of file
diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/MovieInteractor.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/MovieInteractor.kt
index 31db08f27..4870ee9e5 100644
--- a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/MovieInteractor.kt
+++ b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/MovieInteractor.kt
@@ -1,9 +1,7 @@
package org.michaelbel.movies.interactor
import androidx.paging.PagingSource
-import kotlinx.coroutines.flow.Flow
-import org.michaelbel.movies.entities.Either
-import org.michaelbel.movies.network.model.ImagesResponse
+import org.michaelbel.movies.network.Either
import org.michaelbel.movies.network.model.MovieResponse
import org.michaelbel.movies.network.model.Result
import org.michaelbel.movies.persistence.database.entity.MovieDb
@@ -12,14 +10,10 @@ interface MovieInteractor {
fun moviesPagingSource(movieList: String): PagingSource
- fun movieImage(movieId: Int): Flow
-
suspend fun moviesResult(movieList: String, page: Int): Result
suspend fun movieDetails(movieId: Int): Either
- suspend fun movieImages(movieId: Int): ImagesResponse
-
suspend fun removeAllMovies(movieList: String)
suspend fun insertAllMovies(movieList: String, movies: List)
diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/NotificationInteractor.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/NotificationInteractor.kt
new file mode 100644
index 000000000..4923481bf
--- /dev/null
+++ b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/NotificationInteractor.kt
@@ -0,0 +1,8 @@
+package org.michaelbel.movies.interactor
+
+interface NotificationInteractor {
+
+ suspend fun notificationExpireTime(): Long
+
+ suspend fun updateNotificationExpireTime()
+}
\ No newline at end of file
diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/DelayUseCase.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/DelayUseCase.kt
index 2530d7736..818a61ab2 100644
--- a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/DelayUseCase.kt
+++ b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/DelayUseCase.kt
@@ -1,12 +1,13 @@
package org.michaelbel.movies.interactor.usecase
+import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.michaelbel.movies.common.dispatchers.MoviesDispatchers
import org.michaelbel.movies.common.usecase.UseCase
import org.michaelbel.movies.persistence.datastore.MoviesPreferences
-import javax.inject.Inject
+@Deprecated("")
class DelayUseCase @Inject constructor(
dispatchers: MoviesDispatchers,
private val preferences: MoviesPreferences
@@ -17,7 +18,7 @@ class DelayUseCase @Inject constructor(
}
suspend fun networkRequestDelay(): Long {
- return preferences.getNetworkRequestDelay() ?: 0L
+ return preferences.networkRequestDelay() ?: 0L
}
override suspend fun execute(parameters: Int) {
diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/MovieDetailsCase.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/MovieDetailsCase.kt
deleted file mode 100644
index d065e42e1..000000000
--- a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/MovieDetailsCase.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.michaelbel.movies.interactor.usecase
-
-import org.michaelbel.movies.entities.handle
-import org.michaelbel.movies.entities.lce.ScreenState
-import org.michaelbel.movies.interactor.Interactor
-import javax.inject.Inject
-
-class MovieDetailsCase @Inject constructor(
- private val interactor: Interactor
-) {
- suspend operator fun invoke(movieId: Int): ScreenState {
- interactor.movieDetails(movieId).handle(
- success = { movieDetailsData ->
- return ScreenState.Content(movieDetailsData)
- },
- failure = { throwable ->
- return ScreenState.Failure(throwable)
- }
- )
- return ScreenState.Loading
- }
-}
\ No newline at end of file
diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectFeedViewCase.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectFeedViewCase.kt
deleted file mode 100644
index 53eb44eab..000000000
--- a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectFeedViewCase.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.michaelbel.movies.interactor.usecase
-
-import org.michaelbel.movies.common.appearance.FeedView
-import org.michaelbel.movies.interactor.Interactor
-import javax.inject.Inject
-
-class SelectFeedViewCase @Inject constructor(
- private val interactor: Interactor
-) {
- suspend operator fun invoke(feedView: FeedView) {
- interactor.selectFeedView(feedView)
- }
-}
\ No newline at end of file
diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectLanguageCase.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectLanguageCase.kt
deleted file mode 100644
index c2fec7439..000000000
--- a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectLanguageCase.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.michaelbel.movies.interactor.usecase
-
-import org.michaelbel.movies.common.localization.LocaleController
-import org.michaelbel.movies.common.localization.model.AppLanguage
-import javax.inject.Inject
-
-class SelectLanguageCase @Inject constructor(
- private val localeController: LocaleController
-) {
- suspend operator fun invoke(language: AppLanguage) {
- localeController.selectLanguage(language)
- }
-}
\ No newline at end of file
diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectMovieListCase.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectMovieListCase.kt
deleted file mode 100644
index 6abc81a4c..000000000
--- a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectMovieListCase.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.michaelbel.movies.interactor.usecase
-
-import org.michaelbel.movies.common.list.MovieList
-import org.michaelbel.movies.interactor.Interactor
-import javax.inject.Inject
-
-class SelectMovieListCase @Inject constructor(
- private val interactor: Interactor
-) {
- suspend operator fun invoke(movieList: MovieList) {
- interactor.selectMovieList(movieList)
- }
-}
\ No newline at end of file
diff --git a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectThemeCase.kt b/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectThemeCase.kt
deleted file mode 100644
index c91ae35b5..000000000
--- a/core/interactor/src/main/kotlin/org/michaelbel/movies/interactor/usecase/SelectThemeCase.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.michaelbel.movies.interactor.usecase
-
-import org.michaelbel.movies.common.theme.AppTheme
-import org.michaelbel.movies.interactor.Interactor
-import javax.inject.Inject
-
-class SelectThemeCase @Inject constructor(
- private val interactor: Interactor
-) {
- suspend operator fun invoke(appTheme: AppTheme) {
- interactor.selectTheme(appTheme)
- }
-}
\ No newline at end of file
diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts
index 22ae83b00..9a6d6b17a 100644
--- a/core/navigation/build.gradle.kts
+++ b/core/navigation/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
}
android {
diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts
index 085c08be3..82be02b3b 100644
--- a/core/network/build.gradle.kts
+++ b/core/network/build.gradle.kts
@@ -1,18 +1,26 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+
@Suppress("dsl_scope_violation")
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
alias(libs.plugins.kotlin.serialization)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
+val tmdbApiKey: String by lazy {
+ gradleLocalProperties(rootDir).getProperty("TMDB_API_KEY").orEmpty().ifEmpty {
+ System.getenv("TMDB_API_KEY").orEmpty()
+ }
+}
+
android {
namespace = "org.michaelbel.movies.network"
defaultConfig {
minSdk = libs.versions.min.sdk.get().toInt()
compileSdk = libs.versions.compile.sdk.get().toInt()
+ buildConfigField("String", "TMDB_API_KEY", "\"$tmdbApiKey\"")
}
/*buildTypes {
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/Either.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/Either.kt
similarity index 99%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/Either.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/Either.kt
index 25a51f916..d93051de3 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/Either.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/Either.kt
@@ -1,6 +1,6 @@
@file:Suppress("unused")
-package org.michaelbel.movies.entities
+package org.michaelbel.movies.network
/**
* A class that encapsulates a successful result with a value of type T
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/GravatarConfig.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/GravatarConfig.kt
similarity index 61%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/GravatarConfig.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/GravatarConfig.kt
index c1ec50664..86fff4a24 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/GravatarConfig.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/GravatarConfig.kt
@@ -1,3 +1,3 @@
-package org.michaelbel.movies.entities
+package org.michaelbel.movies.network
const val GRAVATAR_URL = "https://www.gravatar.com/avatar/%s"
\ No newline at end of file
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/Response.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/Response.kt
similarity index 83%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/Response.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/Response.kt
index ed6a81848..74375ab8a 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/Response.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/Response.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.entities
+package org.michaelbel.movies.network
suspend fun response(
request: suspend () -> T
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/lce/ScreenState.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/ScreenState.kt
similarity index 81%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/lce/ScreenState.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/ScreenState.kt
index c276cfcc6..e1566d46a 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/lce/ScreenState.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/ScreenState.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.entities.lce
+package org.michaelbel.movies.network
sealed interface ScreenState {
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/TmdbConfig.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/TmdbConfig.kt
similarity index 88%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/TmdbConfig.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/TmdbConfig.kt
index 61c9a0388..1f983f7c2 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/TmdbConfig.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/TmdbConfig.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.entities
+package org.michaelbel.movies.network
const val TMDB_URL = "https://themoviedb.org"
const val TMDB_TERMS_OF_USE = "https://themoviedb.org/documentation/website/terms-of-use"
@@ -7,7 +7,7 @@ const val TMDB_REGISTER = "https://themoviedb.org/signup"
const val TMDB_RESET_PASSWORD = "https://themoviedb.org/reset-password"
const val TMDB_MOVIE_URL = "https://themoviedb.org/movie/%d"
-val tmdbApiKey: String
+private val tmdbApiKey: String
get() = BuildConfig.TMDB_API_KEY
val isTmdbApiKeyEmpty: Boolean
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/TmdbImage.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/TmdbImage.kt
similarity index 69%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/TmdbImage.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/TmdbImage.kt
index ffb16e22c..be830ad9a 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/TmdbImage.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/TmdbImage.kt
@@ -1,4 +1,8 @@
-package org.michaelbel.movies.entities.image
+package org.michaelbel.movies.network
+
+import org.michaelbel.movies.network.model.image.BackdropSize
+import org.michaelbel.movies.network.model.image.PosterSize
+import org.michaelbel.movies.network.model.image.ProfileSize
/**
* See [TMDB Image Basics](https://developer.themoviedb.org/docs/image-basics)
@@ -17,6 +21,7 @@ val String.formatBackdropImage: String
val String.formatProfileImage: String
get() = String.format(TMDB_IMAGE_BASE_URL, ProfileSize.W185.size, this).ifEmpty { IMAGE_EMPTY_URL }
+@Suppress("unused")
val String.original: String
get() {
return when {
@@ -26,4 +31,11 @@ val String.original: String
}
else -> this
}
- }
\ No newline at end of file
+ }
+
+val String.isNotOriginal: Boolean
+ get() = !contains("original".toRegex())
+
+fun String.formatImage(size: String): String {
+ return String.format(TMDB_IMAGE_BASE_URL, size, this).ifEmpty { IMAGE_EMPTY_URL }
+}
\ No newline at end of file
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/lce/ktx/ScreenStateKtx.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktx/ScreenStateKtx.kt
similarity index 62%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/lce/ktx/ScreenStateKtx.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/ktx/ScreenStateKtx.kt
index e24771331..777937c7c 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/lce/ktx/ScreenStateKtx.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktx/ScreenStateKtx.kt
@@ -1,6 +1,6 @@
-package org.michaelbel.movies.entities.lce.ktx
+package org.michaelbel.movies.network.ktx
-import org.michaelbel.movies.entities.lce.ScreenState
+import org.michaelbel.movies.network.ScreenState
val ScreenState.isFailure: Boolean
get() = this is ScreenState.Failure
diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/model/ImagesResponse.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/ImagesResponse.kt
index 760aad0b2..05af803fb 100644
--- a/core/network/src/main/kotlin/org/michaelbel/movies/network/model/ImagesResponse.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/ImagesResponse.kt
@@ -7,6 +7,6 @@ import kotlinx.serialization.Serializable
data class ImagesResponse(
@SerialName("id") val id: Int,
@SerialName("backdrops") val backdrops: List,
- @SerialName("posters") val crew: List,
+ @SerialName("posters") val posters: List,
@SerialName("logos") val logos: List
)
\ No newline at end of file
diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/model/MovieResponse.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/MovieResponse.kt
index f1d1a9332..f07227414 100644
--- a/core/network/src/main/kotlin/org/michaelbel/movies/network/model/MovieResponse.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/MovieResponse.kt
@@ -15,7 +15,6 @@ data class MovieResponse(
@SerialName("genre_ids") val genreIds: List
) {
companion object {
- const val NOW_PLAYING = "now_playing"
const val DEFAULT_PAGE_SIZE = 20
}
}
\ No newline at end of file
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/BackdropSize.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/BackdropSize.kt
similarity index 74%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/BackdropSize.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/BackdropSize.kt
index 417bc2bc7..601819c15 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/BackdropSize.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/BackdropSize.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.entities.image
+package org.michaelbel.movies.network.model.image
@Suppress("unused")
enum class BackdropSize(val size: String) {
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/LogoSize.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/LogoSize.kt
similarity index 79%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/LogoSize.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/LogoSize.kt
index 08928c4e7..8354951d7 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/LogoSize.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/LogoSize.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.entities.image
+package org.michaelbel.movies.network.model.image
@Suppress("unused")
enum class LogoSize(val size: String) {
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/PosterSize.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/PosterSize.kt
similarity index 79%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/PosterSize.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/PosterSize.kt
index 39f2dcefe..bd4b9ace1 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/PosterSize.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/PosterSize.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.entities.image
+package org.michaelbel.movies.network.model.image
@Suppress("unused")
enum class PosterSize(val size: String) {
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/ProfileSize.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/ProfileSize.kt
similarity index 74%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/ProfileSize.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/ProfileSize.kt
index b38fce0ad..0668aea9e 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/ProfileSize.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/ProfileSize.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.entities.image
+package org.michaelbel.movies.network.model.image
@Suppress("unused")
enum class ProfileSize(val size: String) {
diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/StillSize.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/StillSize.kt
similarity index 73%
rename from core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/StillSize.kt
rename to core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/StillSize.kt
index e671832e1..b4b2bffee 100644
--- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/image/StillSize.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/image/StillSize.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.entities.image
+package org.michaelbel.movies.network.model.image
@Suppress("unused")
enum class StillSize(val size: String) {
diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/OkhttpModule.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/OkhttpModule.kt
index bd8d1fc0a..839b6a760 100644
--- a/core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/OkhttpModule.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/OkhttpModule.kt
@@ -7,12 +7,13 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
+import java.util.concurrent.TimeUnit
+import javax.inject.Singleton
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.michaelbel.movies.network.BuildConfig
-import java.util.concurrent.TimeUnit
-import javax.inject.Singleton
+import org.michaelbel.movies.network.okhttp.interceptor.ApikeyInterceptor
@Module
@InstallIn(SingletonComponent::class)
@@ -56,16 +57,24 @@ internal object OkhttpModule {
}
}
+ @Provides
+ @Singleton
+ fun provideApikeyInterceptor(): ApikeyInterceptor {
+ return ApikeyInterceptor(BuildConfig.TMDB_API_KEY)
+ }
+
@Provides
@Singleton
fun provideOkHttp(
chuckerInterceptor: ChuckerInterceptor,
httpLoggingInterceptor: HttpLoggingInterceptor,
+ apikeyInterceptor: ApikeyInterceptor,
cache: Cache
): OkHttpClient {
val builder = OkHttpClient.Builder().apply {
addInterceptor(chuckerInterceptor)
addInterceptor(httpLoggingInterceptor)
+ addInterceptor(apikeyInterceptor)
callTimeout(CALL_TIMEOUT_SECONDS, TimeUnit.SECONDS)
connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/interceptor/ApikeyInterceptor.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/interceptor/ApikeyInterceptor.kt
new file mode 100644
index 000000000..7e86b9d39
--- /dev/null
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/interceptor/ApikeyInterceptor.kt
@@ -0,0 +1,21 @@
+package org.michaelbel.movies.network.okhttp.interceptor
+
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+
+internal class ApikeyInterceptor(
+ private val apiKey: String
+): Interceptor {
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val originalRequest: Request = chain.request()
+ val newHttpUrl = originalRequest.url.newBuilder()
+ .addQueryParameter("api_key", apiKey)
+ .build()
+ val newRequest = originalRequest.newBuilder()
+ .url(newHttpUrl)
+ .build()
+ return chain.proceed(newRequest)
+ }
+}
\ No newline at end of file
diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/account/AccountService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/service/account/AccountService.kt
index 90329c627..41cf6ccee 100644
--- a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/account/AccountService.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/service/account/AccountService.kt
@@ -8,7 +8,6 @@ interface AccountService {
@GET("account")
suspend fun accountDetails(
- @Query("api_key") apiKey: String,
@Query("session_id") sessionId: String
): Account
}
\ No newline at end of file
diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/authentication/AuthenticationService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/service/authentication/AuthenticationService.kt
index c2bff627b..2d89a4530 100644
--- a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/authentication/AuthenticationService.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/service/authentication/AuthenticationService.kt
@@ -10,30 +10,24 @@ import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.HTTP
import retrofit2.http.POST
-import retrofit2.http.Query
interface AuthenticationService {
@GET("authentication/token/new?")
- suspend fun createRequestToken(
- @Query("api_key") apiKey: String
- ): Token
+ suspend fun createRequestToken(): Token
@POST("authentication/token/validate_with_login?")
suspend fun createSessionWithLogin(
- @Query("api_key") apiKey: String,
@Body username: Username
): Token
@POST("authentication/session/new?")
suspend fun createSession(
- @Query("api_key") apiKey: String,
@Body authToken: RequestToken
): Session
@HTTP(method = "DELETE", path = "authentication/session?", hasBody = true)
suspend fun deleteSession(
- @Query("api_key") apiKey: String,
@Body sessionRequest: SessionRequest
): DeletedSession
}
\ No newline at end of file
diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/di/ServiceModule.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/service/di/ServiceModule.kt
index aabf37b0a..48091e385 100644
--- a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/di/ServiceModule.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/service/di/ServiceModule.kt
@@ -4,12 +4,13 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
import org.michaelbel.movies.network.service.account.AccountService
import org.michaelbel.movies.network.service.authentication.AuthenticationService
+import org.michaelbel.movies.network.service.image.ImageService
import org.michaelbel.movies.network.service.ktx.createService
import org.michaelbel.movies.network.service.movie.MovieService
import retrofit2.Retrofit
-import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@@ -21,6 +22,12 @@ internal object ServiceModule {
retrofit: Retrofit
): MovieService = retrofit.createService()
+ @Provides
+ @Singleton
+ fun provideImageService(
+ retrofit: Retrofit
+ ): ImageService = retrofit.createService()
+
@Provides
@Singleton
fun provideAuthenticationService(
diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/image/ImageService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/service/image/ImageService.kt
new file mode 100644
index 000000000..f10543408
--- /dev/null
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/service/image/ImageService.kt
@@ -0,0 +1,13 @@
+package org.michaelbel.movies.network.service.image
+
+import org.michaelbel.movies.network.model.ImagesResponse
+import retrofit2.http.GET
+import retrofit2.http.Path
+
+interface ImageService {
+
+ @GET("movie/{movie_id}/images")
+ suspend fun images(
+ @Path("movie_id") id: Int
+ ): ImagesResponse
+}
\ No newline at end of file
diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/movie/MovieService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/service/movie/MovieService.kt
index 1cc3999b5..51d0f2f91 100644
--- a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/movie/MovieService.kt
+++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/service/movie/MovieService.kt
@@ -1,6 +1,5 @@
package org.michaelbel.movies.network.service.movie
-import org.michaelbel.movies.network.model.ImagesResponse
import org.michaelbel.movies.network.model.Movie
import org.michaelbel.movies.network.model.MovieResponse
import org.michaelbel.movies.network.model.Result
@@ -13,7 +12,6 @@ interface MovieService {
@GET("movie/{list}")
suspend fun movies(
@Path("list") list: String,
- @Query("api_key") apiKey: String,
@Query("language") language: String,
@Query("page") page: Int
): Result
@@ -21,13 +19,6 @@ interface MovieService {
@GET("movie/{movie_id}")
suspend fun movie(
@Path("movie_id") id: Int,
- @Query("api_key") apiKey: String,
@Query("language") language: String
): Movie
-
- @GET("movie/{movie_id}/images")
- suspend fun images(
- @Path("movie_id") id: Int,
- @Query("api_key") apiKey: String,
- ): ImagesResponse
}
\ No newline at end of file
diff --git a/core/notifications/build.gradle.kts b/core/notifications/build.gradle.kts
index f7c2af955..025bcc718 100644
--- a/core/notifications/build.gradle.kts
+++ b/core/notifications/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -38,6 +37,7 @@ android {
dependencies {
implementation(project(":core:common"))
- implementation(project(":core:persistence"))
+ implementation(project(":core:interactor"))
+ implementation(project(":core:ui"))
api(libs.firebase.messaging.ktx)
}
\ No newline at end of file
diff --git a/core/notifications/src/main/AndroidManifest.xml b/core/notifications/src/main/AndroidManifest.xml
index fb8f7993e..10b5a6eea 100644
--- a/core/notifications/src/main/AndroidManifest.xml
+++ b/core/notifications/src/main/AndroidManifest.xml
@@ -16,7 +16,7 @@
+ android:value="@string/notification_new_movies_channel_id" />
diff --git a/core/notifications/src/main/kotlin/org/michaelbel/movies/notifications/NotificationClient.kt b/core/notifications/src/main/kotlin/org/michaelbel/movies/notifications/NotificationClient.kt
index 78ef9cd7c..772501e17 100644
--- a/core/notifications/src/main/kotlin/org/michaelbel/movies/notifications/NotificationClient.kt
+++ b/core/notifications/src/main/kotlin/org/michaelbel/movies/notifications/NotificationClient.kt
@@ -1,34 +1,35 @@
package org.michaelbel.movies.notifications
+import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
+import androidx.annotation.StringRes
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
import kotlinx.coroutines.delay
import org.michaelbel.movies.common.ktx.isTimePasses
+import org.michaelbel.movies.interactor.Interactor
import org.michaelbel.movies.notifications.ktx.isPostNotificationsPermissionGranted
import org.michaelbel.movies.notifications.model.MoviesPush
-import org.michaelbel.movies.persistence.datastore.MoviesPreferences
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
+import org.michaelbel.movies.ui.icons.MoviesIcons
+@SuppressLint("MissingPermission")
class NotificationClient @Inject constructor(
@ApplicationContext private val context: Context,
private val notificationManager: NotificationManagerCompat,
- private val preferences: MoviesPreferences
+ private val interactor: Interactor
) {
- private val channelId: String
- get() = context.getString(R.string.notification_channel_id)
-
suspend fun notificationsPermissionRequired(time: Long): Boolean {
- val expireTime: Long = preferences.getNotificationExpireTime() ?: 0L
+ val expireTime: Long = interactor.notificationExpireTime()
val currentTime: Long = System.currentTimeMillis()
val isTimePasses: Boolean = isTimePasses(ONE_DAY_MILLS, expireTime, currentTime)
delay(time)
@@ -36,17 +37,23 @@ class NotificationClient @Inject constructor(
}
suspend fun updateNotificationExpireTime() {
- val currentTime: Long = System.currentTimeMillis()
- preferences.setNotificationExpireTime(currentTime)
+ interactor.updateNotificationExpireTime()
}
fun send(push: MoviesPush) {
- createChannel()
+ createChannel(
+ channelId = R.string.notification_new_movies_channel_id,
+ channelName = R.string.notification_channel_name,
+ channelDescription = R.string.notification_channel_description
+ )
- val notification = NotificationCompat.Builder(context, channelId).apply {
+ val notification = NotificationCompat.Builder(
+ context,
+ context.getString(R.string.notification_new_movies_channel_id)
+ ).apply {
setContentTitle(push.notificationTitle)
setContentText(push.notificationDescription)
- setSmallIcon(R.drawable.ic_movie_filter_24)
+ setSmallIcon(MoviesIcons.MovieFilter24)
setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL)
color = ContextCompat.getColor(context, R.color.primary)
setDefaults(NotificationCompat.DEFAULT_LIGHTS)
@@ -59,16 +66,59 @@ class NotificationClient @Inject constructor(
setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
}.build()
- notificationManager.notify(TAG, push.notificationId, notification)
+ if (context.isPostNotificationsPermissionGranted) {
+ notificationManager.notify(TAG, push.notificationId, notification)
+ }
+ }
+
+ fun sendDownloadImageNotification(
+ notificationId: Int,
+ @StringRes contentTitleRes: Int,
+ @StringRes contentTextRes: Int
+ ) {
+ createChannel(
+ channelId = R.string.notification_gallery_download_channel_id,
+ channelName = R.string.notification_gallery_channel_name,
+ channelDescription = R.string.notification_gallery_channel_description
+ )
+
+ val notification = NotificationCompat.Builder(
+ context,
+ context.getString(R.string.notification_gallery_download_channel_id)
+ ).apply {
+ setContentTitle(context.getString(contentTitleRes))
+ setContentText(context.getString(contentTextRes))
+ setSmallIcon(MoviesIcons.FileDownload24)
+ setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL)
+ color = ContextCompat.getColor(context, R.color.primary)
+ setDefaults(NotificationCompat.DEFAULT_LIGHTS)
+ setVibrate(VIBRATE_PATTERN)
+ priority = NotificationCompat.PRIORITY_HIGH
+ setSound(null)
+ setOngoing(true)
+ setProgress(0, 0, true)
+ }.build()
+
+ if (context.isPostNotificationsPermissionGranted) {
+ notificationManager.notify(DOWNLOAD_IMAGE_NOTIFICATION_TAG, notificationId, notification)
+ }
+ }
+
+ fun cancelDownloadImageNotification(notificationId: Int) {
+ notificationManager.cancel(DOWNLOAD_IMAGE_NOTIFICATION_TAG, notificationId)
}
- private fun createChannel() {
+ private fun createChannel(
+ @StringRes channelId: Int,
+ @StringRes channelName: Int,
+ @StringRes channelDescription: Int
+ ) {
val notificationChannel: NotificationChannelCompat = NotificationChannelCompat.Builder(
- channelId,
+ context.getString(channelId),
NotificationManagerCompat.IMPORTANCE_HIGH
).apply {
- setName(context.getString(R.string.notification_channel_name))
- setDescription(context.getString(R.string.notification_channel_description))
+ setName(context.getString(channelName))
+ setDescription(context.getString(channelDescription))
setShowBadge(true)
}.build()
notificationManager.createNotificationChannel(notificationChannel)
@@ -85,6 +135,7 @@ class NotificationClient @Inject constructor(
private companion object {
private const val TAG = "PUSH"
+ private const val DOWNLOAD_IMAGE_NOTIFICATION_TAG = "DOWNLOAD_IMAGE"
private const val GROUP_NAME = "App"
private val VIBRATE_PATTERN: LongArray = longArrayOf(1000)
private val ONE_DAY_MILLS: Long = TimeUnit.DAYS.toMillis(1)
diff --git a/core/notifications/src/main/res/values-ru/strings.xml b/core/notifications/src/main/res/values-ru/strings.xml
index dadf2ed46..921fbbff4 100644
--- a/core/notifications/src/main/res/values-ru/strings.xml
+++ b/core/notifications/src/main/res/values-ru/strings.xml
@@ -1,5 +1,7 @@
- Уведомления
- Хорошие уведомления
+ Новые фильмы
+ Уведомления о новых фильмах
+ Загрузка изображений
+ Скачать изображение из галереи
\ No newline at end of file
diff --git a/core/notifications/src/main/res/values/strings.xml b/core/notifications/src/main/res/values/strings.xml
index 25ed1f4b9..f9c5c68cd 100644
--- a/core/notifications/src/main/res/values/strings.xml
+++ b/core/notifications/src/main/res/values/strings.xml
@@ -1,6 +1,9 @@
- movies_channel_id
- Notifications
- Good notifications
+ movies_channel_id
+ gallery_download_id
+ New Movies
+ Notifications about new movies
+ Download Image
+ Download image from gallery
\ No newline at end of file
diff --git a/core/persistence/build.gradle.kts b/core/persistence/build.gradle.kts
index 17cf3065f..242535367 100644
--- a/core/persistence/build.gradle.kts
+++ b/core/persistence/build.gradle.kts
@@ -2,8 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.kotlin.ksp)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -23,6 +21,10 @@ android {
}
}*/
+ buildFeatures {
+ buildConfig = true
+ }
+
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
@@ -38,7 +40,7 @@ android {
}
dependencies {
- implementation(project(":core:entities"))
+ implementation(project(":core:network"))
implementation(libs.bundles.datastore)
implementation(libs.bundles.room)
ksp(libs.androidx.room.compiler)
diff --git a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/AppDatabase.kt b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/AppDatabase.kt
index c47de46b9..9739835c6 100644
--- a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/AppDatabase.kt
+++ b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/AppDatabase.kt
@@ -5,12 +5,14 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
-import org.michaelbel.movies.entities.BuildConfig
+import org.michaelbel.movies.persistence.BuildConfig
import org.michaelbel.movies.persistence.database.converter.CalendarConverter
import org.michaelbel.movies.persistence.database.dao.AccountDao
+import org.michaelbel.movies.persistence.database.dao.ImageDao
import org.michaelbel.movies.persistence.database.dao.MovieDao
import org.michaelbel.movies.persistence.database.dao.PagingKeyDao
import org.michaelbel.movies.persistence.database.entity.AccountDb
+import org.michaelbel.movies.persistence.database.entity.ImageDb
import org.michaelbel.movies.persistence.database.entity.MovieDb
import org.michaelbel.movies.persistence.database.entity.PagingKeyDb
@@ -20,6 +22,7 @@ import org.michaelbel.movies.persistence.database.entity.PagingKeyDb
@Database(
entities = [
MovieDb::class,
+ ImageDb::class,
AccountDb::class,
PagingKeyDb::class
],
@@ -30,12 +33,13 @@ import org.michaelbel.movies.persistence.database.entity.PagingKeyDb
internal abstract class AppDatabase: RoomDatabase() {
abstract fun movieDao(): MovieDao
+ abstract fun imageDao(): ImageDao
abstract fun accountDao(): AccountDao
abstract fun pagingKeyDao(): PagingKeyDao
companion object {
private val DATABASE_NAME: String = if (BuildConfig.DEBUG) "movies-db-debug" else "movies-db"
- const val DATABASE_VERSION = 15
+ const val DATABASE_VERSION = 17
@Volatile
private var instance: AppDatabase? = null
diff --git a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/dao/ImageDao.kt b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/dao/ImageDao.kt
new file mode 100644
index 000000000..32aa91b53
--- /dev/null
+++ b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/dao/ImageDao.kt
@@ -0,0 +1,21 @@
+package org.michaelbel.movies.persistence.database.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import kotlinx.coroutines.flow.Flow
+import org.michaelbel.movies.persistence.database.entity.ImageDb
+
+/**
+ * The Data Access Object for the [ImageDb] class.
+ */
+@Dao
+interface ImageDao {
+
+ @Query("SELECT * FROM images WHERE movieId = :movieId ORDER BY position ASC")
+ fun imagesFlow(movieId: Int): Flow>
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(images: List)
+}
\ No newline at end of file
diff --git a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/dao/MovieDao.kt b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/dao/MovieDao.kt
index 96de4866c..03f41feb7 100644
--- a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/dao/MovieDao.kt
+++ b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/dao/MovieDao.kt
@@ -5,7 +5,6 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
-import kotlinx.coroutines.flow.Flow
import org.michaelbel.movies.persistence.database.entity.MovieDb
/**
@@ -17,9 +16,6 @@ interface MovieDao {
@Query("SELECT * FROM movies WHERE movieList = :movieList ORDER BY position ASC")
fun pagingSource(movieList: String): PagingSource
- @Query("SELECT backdropPath FROM movies WHERE id = :movieId")
- fun movieImage(movieId: Int): Flow
-
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllMovies(movies: List)
diff --git a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/di/DatabaseModule.kt b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/di/DatabaseModule.kt
index 18a011faa..634e60634 100644
--- a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/di/DatabaseModule.kt
+++ b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/di/DatabaseModule.kt
@@ -6,11 +6,12 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
import org.michaelbel.movies.persistence.database.AppDatabase
import org.michaelbel.movies.persistence.database.dao.AccountDao
+import org.michaelbel.movies.persistence.database.dao.ImageDao
import org.michaelbel.movies.persistence.database.dao.MovieDao
import org.michaelbel.movies.persistence.database.dao.PagingKeyDao
-import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@@ -25,6 +26,9 @@ internal object DatabaseModule {
@Provides
fun provideMovieDao(appDatabase: AppDatabase): MovieDao = appDatabase.movieDao()
+ @Provides
+ fun provideImagesDao(appDatabase: AppDatabase): ImageDao = appDatabase.imageDao()
+
@Provides
fun provideAccountDao(appDatabase: AppDatabase): AccountDao = appDatabase.accountDao()
diff --git a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/entity/ImageDb.kt b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/entity/ImageDb.kt
new file mode 100644
index 000000000..f0864920e
--- /dev/null
+++ b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/entity/ImageDb.kt
@@ -0,0 +1,40 @@
+package org.michaelbel.movies.persistence.database.entity
+
+import androidx.room.Entity
+import org.jetbrains.annotations.NotNull
+import org.jetbrains.annotations.Nullable
+
+@Entity(tableName = "images", primaryKeys = ["movieId", "filePath"])
+data class ImageDb(
+ @NotNull val movieId: Int,
+ @NotNull val filePath: String,
+ @NotNull val type: Type,
+ @NotNull val width: Int,
+ @NotNull val height: Int,
+ @NotNull val aspectRatio: Float,
+ @NotNull val voteAverage: Float,
+ @NotNull val voteCount: Int,
+ @Nullable val lang: String?,
+ @NotNull val position: Int
+) {
+ companion object {
+ val Empty: ImageDb = ImageDb(
+ movieId = 0,
+ filePath = "",
+ type = Type.BACKDROP,
+ width = 0,
+ height = 0,
+ aspectRatio = 0F,
+ voteAverage = 0F,
+ voteCount = 0,
+ lang = null,
+ position = 0
+ )
+ }
+
+ enum class Type {
+ BACKDROP,
+ POSTER,
+ LOGO
+ }
+}
\ No newline at end of file
diff --git a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/entity/PagingKeyDb.kt b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/entity/PagingKeyDb.kt
index b54f402bb..15fb3b1b5 100644
--- a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/entity/PagingKeyDb.kt
+++ b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/entity/PagingKeyDb.kt
@@ -2,9 +2,10 @@ package org.michaelbel.movies.persistence.database.entity
import androidx.room.Entity
import org.jetbrains.annotations.NotNull
+import org.jetbrains.annotations.Nullable
@Entity(tableName = "pagingkeys", primaryKeys = ["movieList"])
data class PagingKeyDb(
@NotNull val movieList: String,
- val page: Int? = null
+ @Nullable val page: Int? = null
)
\ No newline at end of file
diff --git a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/ImageDbKtx.kt b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/ImageDbKtx.kt
new file mode 100644
index 000000000..4f84f036b
--- /dev/null
+++ b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/ImageDbKtx.kt
@@ -0,0 +1,21 @@
+package org.michaelbel.movies.persistence.database.ktx
+
+import org.michaelbel.movies.network.formatImage
+import org.michaelbel.movies.network.model.image.BackdropSize
+import org.michaelbel.movies.network.model.image.LogoSize
+import org.michaelbel.movies.network.model.image.PosterSize
+import org.michaelbel.movies.persistence.database.entity.ImageDb
+
+val ImageDb.image: String
+ get() = when (type) {
+ ImageDb.Type.BACKDROP -> filePath.formatImage(BackdropSize.W300.size)
+ ImageDb.Type.POSTER -> filePath.formatImage(PosterSize.W92.size)
+ ImageDb.Type.LOGO -> filePath.formatImage(LogoSize.W45.size)
+ }
+
+val ImageDb.original: String
+ get() = when (type) {
+ ImageDb.Type.BACKDROP -> filePath.formatImage(BackdropSize.ORIGINAL.size)
+ ImageDb.Type.POSTER -> filePath.formatImage(PosterSize.ORIGINAL.size)
+ ImageDb.Type.LOGO -> filePath.formatImage(LogoSize.ORIGINAL.size)
+ }
\ No newline at end of file
diff --git a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/ImageKtx.kt b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/ImageKtx.kt
new file mode 100644
index 000000000..92cbc6895
--- /dev/null
+++ b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/ImageKtx.kt
@@ -0,0 +1,23 @@
+package org.michaelbel.movies.persistence.database.ktx
+
+import org.michaelbel.movies.network.model.Image
+import org.michaelbel.movies.persistence.database.entity.ImageDb
+
+fun Image.imageDb(
+ movieId: Int,
+ type: ImageDb.Type,
+ position: Int
+): ImageDb {
+ return ImageDb(
+ movieId = movieId,
+ filePath = filePath,
+ type = type,
+ width = width,
+ height = height,
+ aspectRatio = aspectRatio,
+ voteAverage = voteAverage,
+ voteCount = voteCount,
+ lang = lang,
+ position = position
+ )
+}
\ No newline at end of file
diff --git a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/MovieDbKtx.kt b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/MovieDbKtx.kt
index 9a5f3398a..263fa173d 100644
--- a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/MovieDbKtx.kt
+++ b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/MovieDbKtx.kt
@@ -1,6 +1,6 @@
package org.michaelbel.movies.persistence.database.ktx
-import org.michaelbel.movies.entities.TMDB_MOVIE_URL
+import org.michaelbel.movies.network.TMDB_MOVIE_URL
import org.michaelbel.movies.persistence.database.entity.MovieDb
import java.util.Locale
diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/MovieResponseKtx.kt b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/MovieResponseKtx.kt
similarity index 52%
rename from core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/MovieResponseKtx.kt
rename to core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/MovieResponseKtx.kt
index 2d901c76e..7f9545d2b 100644
--- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/MovieResponseKtx.kt
+++ b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/database/ktx/MovieResponseKtx.kt
@@ -1,19 +1,17 @@
-package org.michaelbel.movies.domain.ktx
+package org.michaelbel.movies.persistence.database.ktx
-import org.michaelbel.movies.entities.image.formatBackdropImage
-import org.michaelbel.movies.entities.image.formatPosterImage
import org.michaelbel.movies.network.model.MovieResponse
import org.michaelbel.movies.persistence.database.entity.MovieDb
-internal fun MovieResponse.mapToMovieDb(movieList: String, position: Int): MovieDb {
+fun MovieResponse.movieDb(movieList: String, position: Int): MovieDb {
return MovieDb(
movieList = movieList,
dateAdded = System.currentTimeMillis(),
position = position,
movieId = id,
overview = overview.orEmpty(),
- posterPath = posterPath.orEmpty().formatPosterImage,
- backdropPath = backdropPath.orEmpty().formatBackdropImage,
+ posterPath = posterPath.orEmpty(),
+ backdropPath = backdropPath.orEmpty(),
releaseDate = releaseDate,
title = title,
voteAverage = voteAverage
diff --git a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/datastore/MoviesPreferences.kt b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/datastore/MoviesPreferences.kt
index 0911892a4..2b3f81fb5 100644
--- a/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/datastore/MoviesPreferences.kt
+++ b/core/persistence/src/main/kotlin/org/michaelbel/movies/persistence/datastore/MoviesPreferences.kt
@@ -7,10 +7,10 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
+import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
-import javax.inject.Inject
class MoviesPreferences @Inject constructor(
private val dataStore: DataStore
@@ -67,7 +67,7 @@ class MoviesPreferences @Inject constructor(
}
}
- suspend fun getNetworkRequestDelay(): Long? {
+ suspend fun networkRequestDelay(): Long? {
return dataStore.data.first()[PREFERENCE_NETWORK_REQUEST_DELAY_KEY]?.toLong()
}
@@ -77,7 +77,7 @@ class MoviesPreferences @Inject constructor(
}
}
- suspend fun getSessionId(): String? {
+ suspend fun sessionId(): String? {
return dataStore.data.first()[PREFERENCE_SESSION_ID_KEY]
}
@@ -93,7 +93,7 @@ class MoviesPreferences @Inject constructor(
}
}
- suspend fun getAccountId(): Int? {
+ suspend fun accountId(): Int? {
return dataStore.data.first()[PREFERENCE_ACCOUNT_ID_KEY]
}
@@ -109,7 +109,7 @@ class MoviesPreferences @Inject constructor(
}
}
- suspend fun getAccountExpireTime(): Long? {
+ suspend fun accountExpireTime(): Long? {
return dataStore.data.first()[PREFERENCE_ACCOUNT_EXPIRE_TIME_KEY]
}
@@ -119,7 +119,7 @@ class MoviesPreferences @Inject constructor(
}
}
- suspend fun getNotificationExpireTime(): Long? {
+ suspend fun notificationExpireTime(): Long? {
return dataStore.data.first()[PREFERENCE_NOTIFICATION_EXPIRE_TIME_KEY]
}
diff --git a/core/repository-impl/build.gradle.kts b/core/repository-impl/build.gradle.kts
index c7a8c7731..aae27b475 100644
--- a/core/repository-impl/build.gradle.kts
+++ b/core/repository-impl/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -44,4 +43,7 @@ android {
dependencies {
api(project(":core:repository"))
+
+ testImplementation(libs.kotlin.coroutines.test)
+ testImplementation(libs.junit)
}
\ No newline at end of file
diff --git a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/AccountRepositoryImpl.kt b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/AccountRepositoryImpl.kt
index 3daa61e8e..7b80474fa 100644
--- a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/AccountRepositoryImpl.kt
+++ b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/AccountRepositoryImpl.kt
@@ -1,18 +1,17 @@
package org.michaelbel.movies.repository
+import javax.inject.Inject
+import javax.inject.Singleton
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import org.michaelbel.movies.common.exceptions.AccountDetailsException
-import org.michaelbel.movies.entities.tmdbApiKey
import org.michaelbel.movies.network.model.Account
import org.michaelbel.movies.network.service.account.AccountService
import org.michaelbel.movies.persistence.database.dao.AccountDao
import org.michaelbel.movies.persistence.database.entity.AccountDb
import org.michaelbel.movies.persistence.datastore.MoviesPreferences
import org.michaelbel.movies.repository.ktx.mapToAccountDb
-import javax.inject.Inject
-import javax.inject.Singleton
@Singleton
internal class AccountRepositoryImpl @Inject constructor(
@@ -25,13 +24,18 @@ internal class AccountRepositoryImpl @Inject constructor(
.map { accountId -> accountId ?: 0 }
.flatMapLatest(accountDao::accountById)
+ override suspend fun accountId(): Int? {
+ return preferences.accountId()
+ }
+
+ override suspend fun accountExpireTime(): Long? {
+ return preferences.accountExpireTime()
+ }
+
override suspend fun accountDetails() {
try {
- val sessionId: String = preferences.getSessionId().orEmpty()
- val account: Account = accountService.accountDetails(
- apiKey = tmdbApiKey,
- sessionId = sessionId
- )
+ val sessionId: String = preferences.sessionId().orEmpty()
+ val account: Account = accountService.accountDetails(sessionId)
preferences.run {
setAccountId(account.id)
setAccountExpireTime(System.currentTimeMillis())
diff --git a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/AuthenticationRepositoryImpl.kt b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/AuthenticationRepositoryImpl.kt
index bdc89411a..9c0aa81a0 100644
--- a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/AuthenticationRepositoryImpl.kt
+++ b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/AuthenticationRepositoryImpl.kt
@@ -1,10 +1,11 @@
package org.michaelbel.movies.repository
+import javax.inject.Inject
+import javax.inject.Singleton
import org.michaelbel.movies.common.exceptions.CreateRequestTokenException
import org.michaelbel.movies.common.exceptions.CreateSessionException
import org.michaelbel.movies.common.exceptions.CreateSessionWithLoginException
import org.michaelbel.movies.common.exceptions.DeleteSessionException
-import org.michaelbel.movies.entities.tmdbApiKey
import org.michaelbel.movies.network.model.DeletedSession
import org.michaelbel.movies.network.model.RequestToken
import org.michaelbel.movies.network.model.Session
@@ -14,8 +15,6 @@ import org.michaelbel.movies.network.model.Username
import org.michaelbel.movies.network.service.authentication.AuthenticationService
import org.michaelbel.movies.persistence.database.dao.AccountDao
import org.michaelbel.movies.persistence.datastore.MoviesPreferences
-import javax.inject.Inject
-import javax.inject.Singleton
@Singleton
internal class AuthenticationRepositoryImpl @Inject constructor(
@@ -26,7 +25,7 @@ internal class AuthenticationRepositoryImpl @Inject constructor(
override suspend fun createRequestToken(): Token {
return try {
- val token: Token = authenticationService.createRequestToken(tmdbApiKey)
+ val token: Token = authenticationService.createRequestToken()
if (!token.success) {
throw CreateRequestTokenException
}
@@ -43,7 +42,6 @@ internal class AuthenticationRepositoryImpl @Inject constructor(
): Token {
return try {
val token: Token = authenticationService.createSessionWithLogin(
- apiKey = tmdbApiKey,
username = Username(
username = username,
password = password,
@@ -62,7 +60,6 @@ internal class AuthenticationRepositoryImpl @Inject constructor(
override suspend fun createSession(token: String): Session {
return try {
val session: Session = authenticationService.createSession(
- apiKey = tmdbApiKey,
authToken = RequestToken(token)
)
if (session.success) {
@@ -78,14 +75,13 @@ internal class AuthenticationRepositoryImpl @Inject constructor(
override suspend fun deleteSession() {
try {
- val sessionId: String = preferences.getSessionId().orEmpty()
+ val sessionId: String = preferences.sessionId().orEmpty()
val sessionRequest = SessionRequest(sessionId)
val deletedSession: DeletedSession = authenticationService.deleteSession(
- apiKey = tmdbApiKey,
sessionRequest = sessionRequest
)
if (deletedSession.success) {
- val accountId: Int = preferences.getAccountId() ?: 0
+ val accountId: Int = preferences.accountId() ?: 0
accountDao.removeById(accountId)
preferences.run {
removeSessionId()
diff --git a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ImageRepositoryImpl.kt b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ImageRepositoryImpl.kt
new file mode 100644
index 000000000..0eb6dad0a
--- /dev/null
+++ b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ImageRepositoryImpl.kt
@@ -0,0 +1,47 @@
+package org.michaelbel.movies.repository
+
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.flow.Flow
+import org.michaelbel.movies.network.model.ImagesResponse
+import org.michaelbel.movies.network.service.image.ImageService
+import org.michaelbel.movies.persistence.database.dao.ImageDao
+import org.michaelbel.movies.persistence.database.entity.ImageDb
+import org.michaelbel.movies.persistence.database.ktx.imageDb
+
+@Singleton
+internal class ImageRepositoryImpl @Inject constructor(
+ private val imageService: ImageService,
+ private val imageDao: ImageDao
+): ImageRepository {
+
+ override fun imagesFlow(movieId: Int): Flow> {
+ return imageDao.imagesFlow(movieId)
+ }
+
+ override suspend fun images(movieId: Int) {
+ val imageResponse: ImagesResponse = imageService.images(movieId)
+ val posters: List = imageResponse.posters.mapIndexed { index, image ->
+ image.imageDb(
+ movieId = movieId,
+ type = ImageDb.Type.POSTER,
+ position = index
+ )
+ }
+ val backdrops: List = imageResponse.backdrops.mapIndexed { index, image ->
+ image.imageDb(
+ movieId = movieId,
+ type = ImageDb.Type.BACKDROP,
+ position = posters.count().plus(index)
+ )
+ }
+ val logos: List = imageResponse.logos.mapIndexed { index, image ->
+ image.imageDb(
+ movieId = movieId,
+ type = ImageDb.Type.LOGO,
+ position = posters.count().plus(backdrops.count()).plus(index)
+ )
+ }
+ imageDao.insert(posters + backdrops + logos)
+ }
+}
\ No newline at end of file
diff --git a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/MovieRepositoryImpl.kt b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/MovieRepositoryImpl.kt
index a10bc86ad..9e32592bf 100644
--- a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/MovieRepositoryImpl.kt
+++ b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/MovieRepositoryImpl.kt
@@ -3,16 +3,13 @@ package org.michaelbel.movies.repository
import androidx.paging.PagingSource
import javax.inject.Inject
import javax.inject.Singleton
-import kotlinx.coroutines.flow.Flow
import org.michaelbel.movies.common.localization.LocaleController
-import org.michaelbel.movies.entities.Either
-import org.michaelbel.movies.entities.isTmdbApiKeyEmpty
-import org.michaelbel.movies.entities.response
-import org.michaelbel.movies.entities.tmdbApiKey
-import org.michaelbel.movies.network.model.ImagesResponse
+import org.michaelbel.movies.network.Either
+import org.michaelbel.movies.network.isTmdbApiKeyEmpty
import org.michaelbel.movies.network.model.Movie
import org.michaelbel.movies.network.model.MovieResponse
import org.michaelbel.movies.network.model.Result
+import org.michaelbel.movies.network.response
import org.michaelbel.movies.network.service.movie.MovieService
import org.michaelbel.movies.persistence.database.dao.MovieDao
import org.michaelbel.movies.persistence.database.dao.PagingKeyDao
@@ -34,10 +31,6 @@ internal class MovieRepositoryImpl @Inject constructor(
return movieDao.pagingSource(movieList)
}
- override fun movieImage(movieId: Int): Flow {
- return movieDao.movieImage(movieId)
- }
-
override suspend fun moviesResult(movieList: String, page: Int): Result {
if (isTmdbApiKeyEmpty && movieDao.isEmpty(MovieDb.MOVIES_LOCAL_LIST)) {
checkApiKeyNotNullException()
@@ -45,7 +38,6 @@ internal class MovieRepositoryImpl @Inject constructor(
return movieService.movies(
list = movieList,
- apiKey = tmdbApiKey,
language = localeController.language,
page = page
)
@@ -60,7 +52,6 @@ internal class MovieRepositoryImpl @Inject constructor(
} else {
val movie: Movie = movieService.movie(
id = movieId,
- apiKey = tmdbApiKey,
language = localeController.language
)
movie.mapToMovieDb
@@ -68,13 +59,6 @@ internal class MovieRepositoryImpl @Inject constructor(
}
}
- override suspend fun movieImages(movieId: Int): ImagesResponse {
- return movieService.images(
- id = movieId,
- apiKey = tmdbApiKey
- )
- }
-
override suspend fun removeAllMovies(movieList: String) {
movieDao.removeAllMovies(movieList)
}
diff --git a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/NotificationRepositoryImpl.kt b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/NotificationRepositoryImpl.kt
new file mode 100644
index 000000000..55105102f
--- /dev/null
+++ b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/NotificationRepositoryImpl.kt
@@ -0,0 +1,20 @@
+package org.michaelbel.movies.repository
+
+import javax.inject.Inject
+import javax.inject.Singleton
+import org.michaelbel.movies.persistence.datastore.MoviesPreferences
+
+@Singleton
+internal class NotificationRepositoryImpl @Inject constructor(
+ private val preferences: MoviesPreferences
+): NotificationRepository {
+
+ override suspend fun notificationExpireTime(): Long {
+ return preferences.notificationExpireTime() ?: 0L
+ }
+
+ override suspend fun updateNotificationExpireTime() {
+ val currentTime: Long = System.currentTimeMillis()
+ preferences.setNotificationExpireTime(currentTime)
+ }
+}
\ No newline at end of file
diff --git a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/di/RepositoryModule.kt b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/di/RepositoryModule.kt
index bf3dbf1aa..18b60d51c 100644
--- a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/di/RepositoryModule.kt
+++ b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/di/RepositoryModule.kt
@@ -4,15 +4,19 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
import org.michaelbel.movies.repository.AccountRepository
import org.michaelbel.movies.repository.AccountRepositoryImpl
import org.michaelbel.movies.repository.AuthenticationRepository
import org.michaelbel.movies.repository.AuthenticationRepositoryImpl
+import org.michaelbel.movies.repository.ImageRepository
+import org.michaelbel.movies.repository.ImageRepositoryImpl
import org.michaelbel.movies.repository.MovieRepository
import org.michaelbel.movies.repository.MovieRepositoryImpl
+import org.michaelbel.movies.repository.NotificationRepository
+import org.michaelbel.movies.repository.NotificationRepositoryImpl
import org.michaelbel.movies.repository.SettingsRepository
import org.michaelbel.movies.repository.SettingsRepositoryImpl
-import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@@ -20,9 +24,9 @@ internal interface RepositoryModule {
@Binds
@Singleton
- fun provideMovieRepository(
- repository: MovieRepositoryImpl
- ): MovieRepository
+ fun provideAccountRepository(
+ repository: AccountRepositoryImpl
+ ): AccountRepository
@Binds
@Singleton
@@ -32,9 +36,21 @@ internal interface RepositoryModule {
@Binds
@Singleton
- fun provideAccountRepository(
- repository: AccountRepositoryImpl
- ): AccountRepository
+ fun provideImageRepository(
+ repository: ImageRepositoryImpl
+ ): ImageRepository
+
+ @Binds
+ @Singleton
+ fun provideMovieRepository(
+ repository: MovieRepositoryImpl
+ ): MovieRepository
+
+ @Binds
+ @Singleton
+ fun provideNotificationRepository(
+ repository: NotificationRepositoryImpl
+ ): NotificationRepository
@Binds
@Singleton
diff --git a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/AccountKtx.kt b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/AccountKtx.kt
index 4f1b96081..52a0afc9f 100644
--- a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/AccountKtx.kt
+++ b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/AccountKtx.kt
@@ -1,7 +1,7 @@
package org.michaelbel.movies.repository.ktx
-import org.michaelbel.movies.entities.GRAVATAR_URL
-import org.michaelbel.movies.entities.image.formatProfileImage
+import org.michaelbel.movies.network.GRAVATAR_URL
+import org.michaelbel.movies.network.formatProfileImage
import org.michaelbel.movies.network.model.Account
import org.michaelbel.movies.persistence.database.entity.AccountDb
import java.util.Locale
diff --git a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/ExceptionKtx.kt b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/ExceptionKtx.kt
index 66ae7be18..e79dc5063 100644
--- a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/ExceptionKtx.kt
+++ b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/ExceptionKtx.kt
@@ -1,7 +1,7 @@
package org.michaelbel.movies.repository.ktx
import org.michaelbel.movies.common.exceptions.ApiKeyNotNullException
-import org.michaelbel.movies.entities.isTmdbApiKeyEmpty
+import org.michaelbel.movies.network.isTmdbApiKeyEmpty
internal fun checkApiKeyNotNullException() {
if (isTmdbApiKeyEmpty) throw ApiKeyNotNullException
diff --git a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/MovieKtx.kt b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/MovieKtx.kt
index f9d9dda16..e551f9e81 100644
--- a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/MovieKtx.kt
+++ b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/MovieKtx.kt
@@ -1,7 +1,5 @@
package org.michaelbel.movies.repository.ktx
-import org.michaelbel.movies.entities.image.formatBackdropImage
-import org.michaelbel.movies.entities.image.formatPosterImage
import org.michaelbel.movies.network.model.Movie
import org.michaelbel.movies.persistence.database.entity.MovieDb
@@ -12,8 +10,8 @@ internal val Movie.mapToMovieDb: MovieDb
position = 0,
movieId = id,
overview = overview.orEmpty(),
- posterPath = posterPath.orEmpty().formatPosterImage,
- backdropPath = backdropPath.orEmpty().formatBackdropImage,
+ posterPath = posterPath.orEmpty(),
+ backdropPath = backdropPath.orEmpty(),
releaseDate = releaseDate.orEmpty(),
title = title.orEmpty(),
voteAverage = voteAverage
diff --git a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/MovieResponseKtx.kt b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/MovieResponseKtx.kt
index c8fd79fd5..df28664ab 100644
--- a/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/MovieResponseKtx.kt
+++ b/core/repository-impl/src/main/kotlin/org/michaelbel/movies/repository/ktx/MovieResponseKtx.kt
@@ -1,7 +1,5 @@
package org.michaelbel.movies.repository.ktx
-import org.michaelbel.movies.entities.image.formatBackdropImage
-import org.michaelbel.movies.entities.image.formatPosterImage
import org.michaelbel.movies.network.model.MovieResponse
import org.michaelbel.movies.persistence.database.entity.MovieDb
@@ -12,8 +10,8 @@ internal fun MovieResponse.mapToMovieDb(movieList: String, position: Int): Movie
position = position,
movieId = id,
overview = overview.orEmpty(),
- posterPath = posterPath.orEmpty().formatPosterImage,
- backdropPath = backdropPath.orEmpty().formatBackdropImage,
+ posterPath = posterPath.orEmpty(),
+ backdropPath = backdropPath.orEmpty(),
releaseDate = releaseDate,
title = title,
voteAverage = voteAverage
diff --git a/core/repository-impl/src/test/kotlin/org/michaelbel/movies/repository/AccountRepositoryTest.kt b/core/repository-impl/src/test/kotlin/org/michaelbel/movies/repository/AccountRepositoryTest.kt
new file mode 100644
index 000000000..d5e39cbc4
--- /dev/null
+++ b/core/repository-impl/src/test/kotlin/org/michaelbel/movies/repository/AccountRepositoryTest.kt
@@ -0,0 +1,21 @@
+package org.michaelbel.movies.repository
+
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import org.junit.Before
+
+class AccountRepositoryTest {
+
+ private val testScope = TestScope(UnconfinedTestDispatcher())
+
+ private lateinit var subject: AccountRepositoryImpl
+
+ @Before
+ fun setup() {
+ /*subject = AccountRepositoryImpl(
+ accountService = ,
+ accountDao = ,
+ preferences =
+ )*/
+ }
+}
\ No newline at end of file
diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts
index e874ffcfe..6b7717fab 100644
--- a/core/repository/build.gradle.kts
+++ b/core/repository/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -38,7 +37,6 @@ android {
dependencies {
api(project(":core:common"))
- api(project(":core:entities"))
api(project(":core:network"))
api(project(":core:persistence"))
}
\ No newline at end of file
diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/AccountRepository.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/AccountRepository.kt
index 14e30e4aa..ded6f03a2 100644
--- a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/AccountRepository.kt
+++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/AccountRepository.kt
@@ -7,5 +7,9 @@ interface AccountRepository {
val account: Flow
+ suspend fun accountId(): Int?
+
+ suspend fun accountExpireTime(): Long?
+
suspend fun accountDetails()
}
\ No newline at end of file
diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/ImageRepository.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/ImageRepository.kt
new file mode 100644
index 000000000..f80a81280
--- /dev/null
+++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/ImageRepository.kt
@@ -0,0 +1,11 @@
+package org.michaelbel.movies.repository
+
+import kotlinx.coroutines.flow.Flow
+import org.michaelbel.movies.persistence.database.entity.ImageDb
+
+interface ImageRepository {
+
+ fun imagesFlow(movieId: Int): Flow>
+
+ suspend fun images(movieId: Int)
+}
\ No newline at end of file
diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/MovieRepository.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/MovieRepository.kt
index 6d31c18c7..e7532357a 100644
--- a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/MovieRepository.kt
+++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/MovieRepository.kt
@@ -1,9 +1,7 @@
package org.michaelbel.movies.repository
import androidx.paging.PagingSource
-import kotlinx.coroutines.flow.Flow
-import org.michaelbel.movies.entities.Either
-import org.michaelbel.movies.network.model.ImagesResponse
+import org.michaelbel.movies.network.Either
import org.michaelbel.movies.network.model.MovieResponse
import org.michaelbel.movies.network.model.Result
import org.michaelbel.movies.persistence.database.entity.MovieDb
@@ -12,14 +10,10 @@ interface MovieRepository {
fun moviesPagingSource(movieList: String): PagingSource
- fun movieImage(movieId: Int): Flow
-
suspend fun moviesResult(movieList: String, page: Int): Result
suspend fun movieDetails(movieId: Int): Either
- suspend fun movieImages(movieId: Int): ImagesResponse
-
suspend fun removeAllMovies(movieList: String)
suspend fun insertAllMovies(movieList: String, movies: List)
diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/NotificationRepository.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/NotificationRepository.kt
new file mode 100644
index 000000000..371b673b2
--- /dev/null
+++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/NotificationRepository.kt
@@ -0,0 +1,8 @@
+package org.michaelbel.movies.repository
+
+interface NotificationRepository {
+
+ suspend fun notificationExpireTime(): Long
+
+ suspend fun updateNotificationExpireTime()
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/org/michaelbel/movies/core/config/di/RemoteConfigModule.kt b/core/src/main/kotlin/org/michaelbel/movies/core/config/di/RemoteConfigModule.kt
index 786d2def2..209d5b0ed 100644
--- a/core/src/main/kotlin/org/michaelbel/movies/core/config/di/RemoteConfigModule.kt
+++ b/core/src/main/kotlin/org/michaelbel/movies/core/config/di/RemoteConfigModule.kt
@@ -8,7 +8,6 @@ import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
-import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.components.SingletonComponent
import org.michaelbel.movies.core.config.RemoteParams
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index 8076d4719..bf8e75eb0 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
}
android {
@@ -45,9 +44,8 @@ android {
dependencies {
implementation(project(":core:common"))
- implementation(project(":core:domain"))
implementation(project(":core:network"))
- implementation(project(":core:notifications"))
+ implementation(project(":core:persistence"))
api(libs.androidx.core.splashscreen)
api(libs.androidx.constraintlayout.compose)
api(libs.coil.compose)
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/appicon/IconAlias.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/appicon/IconAlias.kt
new file mode 100644
index 000000000..92f5ca2ef
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/appicon/IconAlias.kt
@@ -0,0 +1,37 @@
+package org.michaelbel.movies.ui.appicon
+
+import androidx.annotation.DrawableRes
+import org.michaelbel.movies.ui.R
+
+sealed class IconAlias(
+ val key: String,
+ @DrawableRes val iconRes: Int
+) {
+
+ data object Red: IconAlias(
+ key = RED_ICON_KEY,
+ iconRes = R.drawable.ic_launcher_icon_red
+ )
+
+ data object Purple: IconAlias(
+ key = PURPLE_ICON_KEY,
+ iconRes = R.drawable.ic_launcher_icon_purple
+ )
+
+ data object Brown: IconAlias(
+ key = BROWN_ICON_KEY,
+ iconRes = R.drawable.ic_launcher_icon_brown
+ )
+
+ companion object {
+ private const val RED_ICON_KEY = "RedIcon"
+ private const val PURPLE_ICON_KEY = "PurpleIcon"
+ private const val BROWN_ICON_KEY = "BrownIcon"
+
+ val VALUES: List = listOf(
+ Red,
+ Purple,
+ Brown
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/appicon/MoviesAlias.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/appicon/MoviesAlias.kt
new file mode 100644
index 000000000..9256c31bd
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/appicon/MoviesAlias.kt
@@ -0,0 +1,43 @@
+package org.michaelbel.movies.ui.appicon
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.annotation.DrawableRes
+import org.michaelbel.movies.ui.R
+
+private fun Context.componentName(iconAlias: IconAlias): ComponentName {
+ return ComponentName(packageName, "org.michaelbel.movies.${iconAlias.key}")
+}
+
+fun Context.isEnabled(iconAlias: IconAlias): Boolean {
+ val enabledSetting = packageManager.getComponentEnabledSetting(componentName(iconAlias))
+ return enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED || enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT && iconAlias == IconAlias.Red
+}
+
+fun Context.setIcon(iconAlias: IconAlias) {
+ IconAlias.VALUES.forEach { alias ->
+ packageManager.setComponentEnabledSetting(
+ componentName(alias),
+ if (alias == iconAlias) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP
+ )
+ }
+}
+
+fun Context.installLauncherIcon() {
+ IconAlias.VALUES.forEach { iconAlias ->
+ if (isEnabled(iconAlias)) {
+ return
+ }
+ }
+ setIcon(IconAlias.Red)
+}
+
+internal val Context.shortcutSettingsIconRes: Int
+ @DrawableRes get() = when {
+ isEnabled(IconAlias.Red) -> R.drawable.ic_shortcut_settings_outline_red_48
+ isEnabled(IconAlias.Purple) -> R.drawable.ic_shortcut_settings_outline_purple_48
+ isEnabled(IconAlias.Brown) -> R.drawable.ic_shortcut_settings_outline_brown_48
+ else -> 0
+ }
\ No newline at end of file
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/NotificationBottomSheet.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/NotificationBottomSheet.kt
index f6b71091a..fb4b6ce5f 100644
--- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/NotificationBottomSheet.kt
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/NotificationBottomSheet.kt
@@ -35,7 +35,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import org.michaelbel.movies.notifications.ktx.appNotificationSettingsIntent
+import org.michaelbel.movies.ui.ktx.appNotificationSettingsIntent
import org.michaelbel.movies.ui.R
import org.michaelbel.movies.ui.icons.MoviesIcons
import org.michaelbel.movies.ui.preview.DevicePreviews
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icons/MoviesIcons.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icons/MoviesIcons.kt
index a85098c2f..60d4e1447 100644
--- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icons/MoviesIcons.kt
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icons/MoviesIcons.kt
@@ -6,6 +6,7 @@ import androidx.compose.material.icons.filled.MovieFilter
import androidx.compose.material.icons.outlined.AccountCircle
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Close
+import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.GridView
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Language
@@ -26,14 +27,15 @@ import org.michaelbel.movies.ui.R
object MoviesIcons {
@DrawableRes val TmdbLogo: Int = R.drawable.ic_tmdb_logo
@DrawableRes val ThemeLightDark: Int = R.drawable.ic_theme_light_dark_24
- @DrawableRes val NotificationSmallIconMovieFilter = R.drawable.ic_movie_filter_24
+ @DrawableRes val MovieFilter24 = R.drawable.ic_movie_filter_24
+ @DrawableRes val FileDownload24 = R.drawable.ic_file_download_24
@DrawableRes val AdultOutline = R.drawable.ic_18_up_rating_outline_24
- @DrawableRes val ShortcutSettingsOutline: Int = R.drawable.ic_shortcut_settings_outline_48
val Account: ImageVector = Icons.Outlined.AccountCircle
val ArrowBack: ImageVector = Icons.Outlined.ArrowBack
val Close: ImageVector = Icons.Outlined.Close
val Info: ImageVector = Icons.Outlined.Info
+ val FileDownload: ImageVector = Icons.Outlined.FileDownload
val GridView: ImageVector = Icons.Outlined.GridView
val Language: ImageVector = Icons.Outlined.Language
val LocationOn: ImageVector = Icons.Outlined.LocationOn
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/ktx/ConfigurationKtx.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/ktx/ConfigurationKtx.kt
new file mode 100644
index 000000000..d1fc2aeab
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/ktx/ConfigurationKtx.kt
@@ -0,0 +1,16 @@
+package org.michaelbel.movies.ui.ktx
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.displayCutout
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalConfiguration
+
+val isPortrait: Boolean
+ @Composable get() {
+ val configuration = LocalConfiguration.current
+ return configuration.orientation == Configuration.ORIENTATION_PORTRAIT
+ }
+
+val displayCutoutWindowInsets: WindowInsets
+ @Composable get() = if (isPortrait) WindowInsets(0, 0, 0, 0) else WindowInsets.displayCutout
\ No newline at end of file
diff --git a/core/notifications/src/main/kotlin/org/michaelbel/movies/notifications/ktx/SettingsKtx.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/ktx/SettingsKtx.kt
similarity index 67%
rename from core/notifications/src/main/kotlin/org/michaelbel/movies/notifications/ktx/SettingsKtx.kt
rename to core/ui/src/main/kotlin/org/michaelbel/movies/ui/ktx/SettingsKtx.kt
index 2783b73f3..c6f51cab5 100644
--- a/core/notifications/src/main/kotlin/org/michaelbel/movies/notifications/ktx/SettingsKtx.kt
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/ktx/SettingsKtx.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.notifications.ktx
+package org.michaelbel.movies.ui.ktx
import android.content.Context
import android.content.Intent
@@ -11,18 +11,18 @@ val Context.appNotificationSettingsIntent: Intent
val intent = Intent()
when {
Build.VERSION.SDK_INT >= 26 -> {
- intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
+ intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
}
Build.VERSION.SDK_INT >= 21 -> {
- intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS")
+ intent.action = "android.settings.APP_NOTIFICATION_SETTINGS"
intent.putExtra("app_package", packageName)
intent.putExtra("app_uid", applicationInfo.uid)
}
else -> {
- intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
intent.addCategory(Intent.CATEGORY_DEFAULT)
- intent.setData("package:$packageName".toUri())
+ intent.data = "package:$packageName".toUri()
}
}
return intent
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/DeviceLandscapePreviews.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/DeviceLandscapePreviews.kt
new file mode 100644
index 000000000..6b74e196a
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/DeviceLandscapePreviews.kt
@@ -0,0 +1,18 @@
+package org.michaelbel.movies.ui.preview
+
+import android.content.res.Configuration
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview(
+ name = "Day Landscape",
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+ widthDp = 800,
+ heightDp = 360
+)
+@Preview(
+ name = "Night Landscape",
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ widthDp = 800,
+ heightDp = 360
+)
+annotation class DeviceLandscapePreviews
\ No newline at end of file
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/DevicePreviews.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/DevicePreviews.kt
index 818d86aa9..b87e3e56b 100644
--- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/DevicePreviews.kt
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/DevicePreviews.kt
@@ -4,11 +4,11 @@ import android.content.res.Configuration
import androidx.compose.ui.tooling.preview.Preview
@Preview(
- uiMode = Configuration.UI_MODE_NIGHT_NO,
- name = "Day theme"
+ name = "Day theme",
+ uiMode = Configuration.UI_MODE_NIGHT_NO
)
@Preview(
- uiMode = Configuration.UI_MODE_NIGHT_YES,
- name = "Night theme"
+ name = "Night theme",
+ uiMode = Configuration.UI_MODE_NIGHT_YES
)
annotation class DevicePreviews
\ No newline at end of file
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/DeviceUserPreviews.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/DeviceUserPreviews.kt
new file mode 100644
index 000000000..c182a9e97
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/DeviceUserPreviews.kt
@@ -0,0 +1,26 @@
+package org.michaelbel.movies.ui.preview
+
+import android.content.res.Configuration
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview(
+ name = "Day",
+ uiMode = Configuration.UI_MODE_NIGHT_NO
+)
+@Preview(
+ name = "Night",
+ uiMode = Configuration.UI_MODE_NIGHT_YES
+)
+@Preview(
+ name = "Day Landscape",
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+ widthDp = 800,
+ heightDp = 360
+)
+@Preview(
+ name = "Night Landscape",
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ widthDp = 800,
+ heightDp = 360
+)
+annotation class DeviceUserPreviews
\ No newline at end of file
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/provider/IconAliasPreviewParameterProvider.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/provider/IconAliasPreviewParameterProvider.kt
new file mode 100644
index 000000000..416261ce0
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/preview/provider/IconAliasPreviewParameterProvider.kt
@@ -0,0 +1,8 @@
+package org.michaelbel.movies.ui.preview.provider
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import org.michaelbel.movies.ui.appicon.IconAlias
+
+class IconAliasPreviewParameterProvider: PreviewParameterProvider {
+ override val values: Sequence = IconAlias.VALUES.asSequence()
+}
\ No newline at end of file
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/shortcuts/MoviesShortcuts.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/shortcuts/MoviesShortcuts.kt
index 594e3154b..c596a1f9f 100644
--- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/shortcuts/MoviesShortcuts.kt
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/shortcuts/MoviesShortcuts.kt
@@ -7,7 +7,7 @@ import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import org.michaelbel.movies.ui.R
-import org.michaelbel.movies.ui.icons.MoviesIcons
+import org.michaelbel.movies.ui.appicon.shortcutSettingsIconRes
private const val SETTINGS_SHORTCUT_ID = "settingsShortcutId"
@@ -21,7 +21,7 @@ fun Context.installShortcuts() {
.setShortLabel(getString(R.string.shortcuts_settings_title))
.setLongLabel(getString(R.string.shortcuts_settings_title))
.setRank(1)
- .setIcon(IconCompat.createWithResource(this, MoviesIcons.ShortcutSettingsOutline))
+ .setIcon(IconCompat.createWithResource(this, shortcutSettingsIconRes))
.setIntent(Intent(Intent.ACTION_VIEW, INTENT_ACTION_SETTINGS.toUri()))
.build()
ShortcutManagerCompat.pushDynamicShortcut(this, settingsShortcut)
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/Theme.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/Theme.kt
index fffc41139..2a050ee40 100644
--- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/Theme.kt
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/Theme.kt
@@ -2,15 +2,18 @@ package org.michaelbel.movies.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import com.google.accompanist.systemuicontroller.SystemUiController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import org.michaelbel.movies.common.theme.AppTheme
import org.michaelbel.movies.ui.ktx.context
import org.michaelbel.movies.ui.theme.model.ComposeTheme
+import org.michaelbel.movies.ui.theme.provider.MoviesRippleTheme
@Composable
fun MoviesTheme(
@@ -18,10 +21,9 @@ fun MoviesTheme(
dynamicColors: Boolean = false,
content: @Composable () -> Unit
) {
- val systemUiController: SystemUiController = rememberSystemUiController()
val dynamicColorsAvailable: Boolean = Build.VERSION.SDK_INT >= 31
- val composeTheme: ComposeTheme = when (theme) {
+ val (colorScheme, statusBarDarkContentEnabled) = when (theme) {
AppTheme.NightNo -> {
ComposeTheme(
colorScheme = if (dynamicColorsAvailable && dynamicColors) {
@@ -64,14 +66,17 @@ fun MoviesTheme(
}
}
- val (colorScheme, statusBarDarkContentEnabled) = composeTheme
-
+ val systemUiController: SystemUiController = rememberSystemUiController()
systemUiController.statusBarDarkContentEnabled = statusBarDarkContentEnabled
MaterialTheme(
colorScheme = colorScheme,
shapes = MoviesShapes,
- typography = MoviesTypography,
- content = content
- )
+ typography = MoviesTypography
+ ) {
+ CompositionLocalProvider(
+ LocalRippleTheme provides MoviesRippleTheme,
+ content = content
+ )
+ }
}
\ No newline at end of file
diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/provider/MoviesRippleTheme.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/provider/MoviesRippleTheme.kt
new file mode 100644
index 000000000..fb86f6bec
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/provider/MoviesRippleTheme.kt
@@ -0,0 +1,20 @@
+package org.michaelbel.movies.ui.theme.provider
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.ripple.RippleAlpha
+import androidx.compose.material.ripple.RippleTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+internal object MoviesRippleTheme: RippleTheme {
+
+ @Composable
+ override fun defaultColor(): Color = MaterialTheme.colorScheme.primary
+
+ @Composable
+ override fun rippleAlpha(): RippleAlpha = RippleTheme.defaultRippleAlpha(
+ contentColor = Color.Black,
+ lightTheme = !isSystemInDarkTheme()
+ )
+}
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable/ic_file_download_24.xml b/core/ui/src/main/res/drawable/ic_file_download_24.xml
new file mode 100644
index 000000000..b29fff816
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_file_download_24.xml
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable/ic_launcher_icon_brown.xml b/core/ui/src/main/res/drawable/ic_launcher_icon_brown.xml
new file mode 100644
index 000000000..d92774e60
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_launcher_icon_brown.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable/ic_launcher_icon_purple.xml b/core/ui/src/main/res/drawable/ic_launcher_icon_purple.xml
new file mode 100644
index 000000000..d130fc1aa
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_launcher_icon_purple.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable/ic_launcher_icon_red.xml b/core/ui/src/main/res/drawable/ic_launcher_icon_red.xml
new file mode 100644
index 000000000..5f85b55f0
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_launcher_icon_red.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable/ic_launcher_monochrome.xml b/core/ui/src/main/res/drawable/ic_launcher_monochrome.xml
deleted file mode 100644
index 1fda1b29a..000000000
--- a/core/ui/src/main/res/drawable/ic_launcher_monochrome.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable/ic_shortcut_settings_outline_brown_48.xml b/core/ui/src/main/res/drawable/ic_shortcut_settings_outline_brown_48.xml
new file mode 100644
index 000000000..96f4825e1
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_shortcut_settings_outline_brown_48.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable/ic_shortcut_settings_outline_purple_48.xml b/core/ui/src/main/res/drawable/ic_shortcut_settings_outline_purple_48.xml
new file mode 100644
index 000000000..a88c04d48
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_shortcut_settings_outline_purple_48.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable/ic_shortcut_settings_outline_48.xml b/core/ui/src/main/res/drawable/ic_shortcut_settings_outline_red_48.xml
similarity index 100%
rename from core/ui/src/main/res/drawable/ic_shortcut_settings_outline_48.xml
rename to core/ui/src/main/res/drawable/ic_shortcut_settings_outline_red_48.xml
diff --git a/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index 2471860a4..000000000
--- a/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_brown.xml b/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_brown.xml
new file mode 100644
index 000000000..b902e6101
--- /dev/null
+++ b/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_brown.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_purple.xml b/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_purple.xml
new file mode 100644
index 000000000..7ba6102c9
--- /dev/null
+++ b/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_purple.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_red.xml b/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_red.xml
new file mode 100644
index 000000000..37d217715
--- /dev/null
+++ b/core/ui/src/main/res/mipmap-anydpi-v26/ic_launcher_red.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 2d09709eb..000000000
Binary files a/core/ui/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_background_brown.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_background_brown.png
new file mode 100644
index 000000000..e9974db04
Binary files /dev/null and b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_background_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_background_purple.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_background_purple.png
new file mode 100644
index 000000000..880627875
Binary files /dev/null and b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_background_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_background.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_background_red.png
similarity index 100%
rename from core/ui/src/main/res/mipmap-hdpi/ic_launcher_background.png
rename to core/ui/src/main/res/mipmap-hdpi/ic_launcher_background_red.png
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_brown.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_brown.png
new file mode 100644
index 000000000..344f3558b
Binary files /dev/null and b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
deleted file mode 100644
index dbd884e7f..000000000
Binary files a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground_brown.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground_brown.png
new file mode 100644
index 000000000..498ecbf45
Binary files /dev/null and b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground_purple.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground_purple.png
new file mode 100644
index 000000000..498ecbf45
Binary files /dev/null and b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground_red.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground_red.png
new file mode 100644
index 000000000..498ecbf45
Binary files /dev/null and b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_foreground_red.png differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_monochrome_brown.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_monochrome_brown.png
new file mode 100644
index 000000000..db0b70a69
Binary files /dev/null and b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_monochrome_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_monochrome_purple.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_monochrome_purple.png
new file mode 100644
index 000000000..db0b70a69
Binary files /dev/null and b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_monochrome_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_monochrome_red.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_monochrome_red.png
new file mode 100644
index 000000000..db0b70a69
Binary files /dev/null and b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_monochrome_red.png differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_purple.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_purple.png
new file mode 100644
index 000000000..99f874d5c
Binary files /dev/null and b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-hdpi/ic_launcher_red.png b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_red.png
new file mode 100644
index 000000000..a12fe5576
Binary files /dev/null and b/core/ui/src/main/res/mipmap-hdpi/ic_launcher_red.png differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index a88995f9f..000000000
Binary files a/core/ui/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_background_brown.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_background_brown.png
new file mode 100644
index 000000000..1159b6dbd
Binary files /dev/null and b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_background_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_background_purple.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_background_purple.png
new file mode 100644
index 000000000..3db74601a
Binary files /dev/null and b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_background_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_background.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_background_red.png
similarity index 100%
rename from core/ui/src/main/res/mipmap-mdpi/ic_launcher_background.png
rename to core/ui/src/main/res/mipmap-mdpi/ic_launcher_background_red.png
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_brown.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_brown.png
new file mode 100644
index 000000000..45d4d15a4
Binary files /dev/null and b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
deleted file mode 100644
index a5bf27034..000000000
Binary files a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground_brown.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground_brown.png
new file mode 100644
index 000000000..e8dfeca29
Binary files /dev/null and b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground_purple.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground_purple.png
new file mode 100644
index 000000000..e8dfeca29
Binary files /dev/null and b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground_red.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground_red.png
new file mode 100644
index 000000000..e8dfeca29
Binary files /dev/null and b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_foreground_red.png differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_monochrome_brown.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_monochrome_brown.png
new file mode 100644
index 000000000..b516cfdc8
Binary files /dev/null and b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_monochrome_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_monochrome_purple.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_monochrome_purple.png
new file mode 100644
index 000000000..b516cfdc8
Binary files /dev/null and b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_monochrome_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_monochrome_red.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_monochrome_red.png
new file mode 100644
index 000000000..b516cfdc8
Binary files /dev/null and b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_monochrome_red.png differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_purple.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_purple.png
new file mode 100644
index 000000000..55d6be8df
Binary files /dev/null and b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-mdpi/ic_launcher_red.png b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_red.png
new file mode 100644
index 000000000..983a5086f
Binary files /dev/null and b/core/ui/src/main/res/mipmap-mdpi/ic_launcher_red.png differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index bfa7857f1..000000000
Binary files a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_background_brown.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_background_brown.png
new file mode 100644
index 000000000..6bec93e24
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_background_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_background_purple.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_background_purple.png
new file mode 100644
index 000000000..979dda7a1
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_background_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_background_red.png
similarity index 100%
rename from core/ui/src/main/res/mipmap-xhdpi/ic_launcher_background.png
rename to core/ui/src/main/res/mipmap-xhdpi/ic_launcher_background_red.png
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_brown.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_brown.png
new file mode 100644
index 000000000..d4540a649
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
deleted file mode 100644
index f06b5505d..000000000
Binary files a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground_brown.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground_brown.png
new file mode 100644
index 000000000..de4a97a3e
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground_purple.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground_purple.png
new file mode 100644
index 000000000..de4a97a3e
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground_red.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground_red.png
new file mode 100644
index 000000000..de4a97a3e
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground_red.png differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_brown.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_brown.png
new file mode 100644
index 000000000..4194a4712
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_purple.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_purple.png
new file mode 100644
index 000000000..4194a4712
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_red.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_red.png
new file mode 100644
index 000000000..4194a4712
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_red.png differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_purple.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_purple.png
new file mode 100644
index 000000000..7d9d7cbce
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_red.png b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_red.png
new file mode 100644
index 000000000..4cea7a968
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xhdpi/ic_launcher_red.png differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 3d725d77f..000000000
Binary files a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_background_brown.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_background_brown.png
new file mode 100644
index 000000000..c40bce6bd
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_background_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_background_purple.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_background_purple.png
new file mode 100644
index 000000000..2a774a0d2
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_background_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_background_red.png
similarity index 100%
rename from core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
rename to core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_background_red.png
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_brown.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_brown.png
new file mode 100644
index 000000000..213eaa6b7
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 5649a57f2..000000000
Binary files a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_brown.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_brown.png
new file mode 100644
index 000000000..774c2726a
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_purple.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_purple.png
new file mode 100644
index 000000000..774c2726a
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_red.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_red.png
new file mode 100644
index 000000000..774c2726a
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_red.png differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_brown.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_brown.png
new file mode 100644
index 000000000..c84b0e750
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_purple.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_purple.png
new file mode 100644
index 000000000..c84b0e750
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_red.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_red.png
new file mode 100644
index 000000000..c84b0e750
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_red.png differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_purple.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_purple.png
new file mode 100644
index 000000000..e489ec508
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_red.png b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_red.png
new file mode 100644
index 000000000..b13a526e1
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxhdpi/ic_launcher_red.png differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 0dbaef1fe..000000000
Binary files a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_background_brown.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_background_brown.png
new file mode 100644
index 000000000..afc69447d
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_background_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_background_purple.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_background_purple.png
new file mode 100644
index 000000000..8c9b3b828
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_background_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_background_red.png
similarity index 100%
rename from core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
rename to core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_background_red.png
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_brown.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_brown.png
new file mode 100644
index 000000000..227c65465
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 82b08bc67..000000000
Binary files a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_brown.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_brown.png
new file mode 100644
index 000000000..9adfeb081
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_purple.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_purple.png
new file mode 100644
index 000000000..9adfeb081
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_red.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_red.png
new file mode 100644
index 000000000..9adfeb081
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_red.png differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_brown.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_brown.png
new file mode 100644
index 000000000..4c384332d
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_brown.png differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_purple.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_purple.png
new file mode 100644
index 000000000..4c384332d
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_red.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_red.png
new file mode 100644
index 000000000..4c384332d
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_red.png differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_purple.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_purple.png
new file mode 100644
index 000000000..c025cc695
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_purple.png differ
diff --git a/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_red.png b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_red.png
new file mode 100644
index 000000000..aa7251859
Binary files /dev/null and b/core/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_red.png differ
diff --git a/core/domain/.gitignore b/core/work/.gitignore
similarity index 100%
rename from core/domain/.gitignore
rename to core/work/.gitignore
diff --git a/core/domain/build.gradle.kts b/core/work/build.gradle.kts
similarity index 77%
rename from core/domain/build.gradle.kts
rename to core/work/build.gradle.kts
index 996979829..238843ee0 100644
--- a/core/domain/build.gradle.kts
+++ b/core/work/build.gradle.kts
@@ -7,7 +7,7 @@ plugins {
}
android {
- namespace = "org.michaelbel.movies.domain"
+ namespace = "org.michaelbel.movies.work"
defaultConfig {
minSdk = libs.versions.min.sdk.get().toInt()
@@ -24,8 +24,7 @@ android {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + listOf(
- "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
- "-opt-in=androidx.paging.ExperimentalPagingApi"
+ "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
)
}
@@ -44,14 +43,14 @@ android {
}
dependencies {
- api(project(":core:interactor-impl"))
- api(project(":core:persistence"))
- implementation(project(":core:analytics"))
+ implementation(project(":core:interactor-impl"))
implementation(project(":core:common"))
- implementation(project(":core:entities"))
implementation(project(":core:network"))
+ implementation(project(":core:notifications"))
implementation(project(":core:repository-impl"))
- api(libs.androidx.hilt.work)
- api(libs.androidx.work.runtime.ktx)
+ implementation(project(":core:ui"))
implementation(libs.androidx.paging.compose)
+ implementation(libs.androidx.hilt.work)
+ implementation(libs.androidx.work.runtime.ktx)
+ ksp(libs.androidx.hilt.compiler)
}
\ No newline at end of file
diff --git a/core/domain/src/main/AndroidManifest.xml b/core/work/src/main/AndroidManifest.xml
similarity index 100%
rename from core/domain/src/main/AndroidManifest.xml
rename to core/work/src/main/AndroidManifest.xml
diff --git a/core/domain/src/main/assets/movies.json b/core/work/src/main/assets/movies.json
similarity index 100%
rename from core/domain/src/main/assets/movies.json
rename to core/work/src/main/assets/movies.json
diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/AccountUpdateWorker.kt b/core/work/src/main/kotlin/org/michaelbel/movies/work/AccountUpdateWorker.kt
similarity index 74%
rename from core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/AccountUpdateWorker.kt
rename to core/work/src/main/kotlin/org/michaelbel/movies/work/AccountUpdateWorker.kt
index 5718ccead..e1947550a 100644
--- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/AccountUpdateWorker.kt
+++ b/core/work/src/main/kotlin/org/michaelbel/movies/work/AccountUpdateWorker.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.domain.workers
+package org.michaelbel.movies.work
import android.content.Context
import androidx.hilt.work.HiltWorker
@@ -6,28 +6,26 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
+import java.util.concurrent.TimeUnit
import org.michaelbel.movies.common.ktx.isTimePasses
-import org.michaelbel.movies.entities.isTmdbApiKeyEmpty
import org.michaelbel.movies.interactor.Interactor
-import org.michaelbel.movies.persistence.datastore.MoviesPreferences
-import java.util.concurrent.TimeUnit
+import org.michaelbel.movies.network.isTmdbApiKeyEmpty
@HiltWorker
class AccountUpdateWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
- private val interactor: Interactor,
- private val preferences: MoviesPreferences
+ private val interactor: Interactor
): CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
return try {
- val accountId: Int? = preferences.getAccountId()
+ val accountId: Int? = interactor.accountId()
if (isTmdbApiKeyEmpty || accountId == null) {
return Result.success()
}
- val expireTime: Long = preferences.getAccountExpireTime() ?: 0L
+ val expireTime: Long = interactor.accountExpireTime() ?: 0L
val currentTime: Long = System.currentTimeMillis()
if (isTimePasses(ONE_DAY_MILLS, expireTime, currentTime)) {
interactor.accountDetails()
diff --git a/core/work/src/main/kotlin/org/michaelbel/movies/work/DownloadImageWorker.kt b/core/work/src/main/kotlin/org/michaelbel/movies/work/DownloadImageWorker.kt
new file mode 100644
index 000000000..62f3c86f4
--- /dev/null
+++ b/core/work/src/main/kotlin/org/michaelbel/movies/work/DownloadImageWorker.kt
@@ -0,0 +1,102 @@
+package org.michaelbel.movies.work
+
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import androidx.core.net.toUri
+import androidx.hilt.work.HiltWorker
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import androidx.work.workDataOf
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import java.io.File
+import java.io.FileOutputStream
+import java.net.URL
+import org.michaelbel.movies.common.ktx.currentDateTime
+import org.michaelbel.movies.notifications.NotificationClient
+
+@HiltWorker
+class DownloadImageWorker @AssistedInject constructor(
+ @Assisted private val context: Context,
+ @Assisted private val workerParams: WorkerParameters,
+ private val notificationClient: NotificationClient
+): CoroutineWorker(context, workerParams) {
+
+ override suspend fun doWork(): Result {
+ val imageUrl: String = inputData.getString(KEY_IMAGE_URL).orEmpty()
+ val contentTitleRes: Int = inputData.getInt(KEY_CONTENT_TITLE, 0)
+ val contentTextRes: Int = inputData.getInt(KEY_CONTENT_TEXT, 0)
+ val notificationId: Int = imageUrl.hashCode()
+
+ if (imageUrl.isEmpty()) {
+ Result.failure(workDataOf(KEY_IMAGE_URL to FAILURE_RESULT))
+ }
+
+ notificationClient.sendDownloadImageNotification(
+ notificationId = notificationId,
+ contentTitleRes = contentTitleRes,
+ contentTextRes = contentTextRes
+ )
+
+ val uri: Uri? = saveImageToDownloads(
+ url = imageUrl,
+ name = "$currentDateTime.jpg"
+ )
+
+ notificationClient.cancelDownloadImageNotification(notificationId)
+
+ return if (uri != null) {
+ Result.success(workDataOf(KEY_IMAGE_URL to uri.toString()))
+ } else {
+ Result.failure(workDataOf(KEY_IMAGE_URL to FAILURE_RESULT))
+ }
+ }
+
+ private fun saveImageToDownloads(url: String, name: String): Uri? {
+ if (Build.VERSION.SDK_INT >= 29) {
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, name)
+ put(MediaStore.MediaColumns.MIME_TYPE, IMAGE_MIME_TYPE)
+ put(MediaStore.MediaColumns.RELATIVE_PATH, IMAGE_RELATIVE_PATH)
+ }
+ val contentResolver: ContentResolver = context.contentResolver
+ val uri: Uri? = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
+ return if (uri != null) {
+ URL(url).openStream().use { input ->
+ contentResolver.openOutputStream(uri).use { output ->
+ input.copyTo(requireNotNull(output), DEFAULT_BUFFER_SIZE)
+ }
+ }
+ uri
+ } else {
+ null
+ }
+ } else {
+ val file = File(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
+ name
+ )
+ URL(url).openStream().use { input ->
+ FileOutputStream(file).use { output ->
+ input.copyTo(output)
+ }
+ }
+ return file.toUri()
+ }
+ }
+
+ companion object {
+ const val KEY_IMAGE_URL = "IMAGE_URL"
+ const val KEY_CONTENT_TITLE = "CONTENT_TITLE"
+ const val KEY_CONTENT_TEXT = "CONTENT_TEXT"
+ const val FAILURE_RESULT = "FAILURE_RESULT"
+ const val IMAGE_MIME_TYPE = "JPG"
+ const val IMAGE_RELATIVE_PATH = "Download"
+ const val DOWNLOAD_IMAGE_WORKER_TAG = "downloadImageWorker"
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/MoviesDatabaseWorker.kt b/core/work/src/main/kotlin/org/michaelbel/movies/work/MoviesDatabaseWorker.kt
similarity index 93%
rename from core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/MoviesDatabaseWorker.kt
rename to core/work/src/main/kotlin/org/michaelbel/movies/work/MoviesDatabaseWorker.kt
index 07b6b782f..47421311e 100644
--- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/MoviesDatabaseWorker.kt
+++ b/core/work/src/main/kotlin/org/michaelbel/movies/work/MoviesDatabaseWorker.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.domain.workers
+package org.michaelbel.movies.work
import android.content.Context
import androidx.hilt.work.HiltWorker
@@ -10,11 +10,11 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.michaelbel.movies.common.dispatchers.MoviesDispatchers
-import org.michaelbel.movies.domain.ktx.mapToMovieDb
import org.michaelbel.movies.network.model.MovieResponse
import org.michaelbel.movies.persistence.database.dao.MovieDao
import org.michaelbel.movies.persistence.database.dao.ktx.isEmpty
import org.michaelbel.movies.persistence.database.entity.MovieDb
+import org.michaelbel.movies.persistence.database.ktx.movieDb
@HiltWorker
class MoviesDatabaseWorker @AssistedInject constructor(
@@ -33,7 +33,7 @@ class MoviesDatabaseWorker @AssistedInject constructor(
val format = Json { ignoreUnknownKeys = true }
val moviesJsonData: List = format.decodeFromStream(inputStream)
val moviesDb: List = moviesJsonData.mapIndexed { index, movieResponse ->
- movieResponse.mapToMovieDb(
+ movieResponse.movieDb(
movieList = MovieDb.MOVIES_LOCAL_LIST,
position = index.plus(1)
)
diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/di/WorkManagerModule.kt b/core/work/src/main/kotlin/org/michaelbel/movies/work/di/WorkModule.kt
similarity index 67%
rename from core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/di/WorkManagerModule.kt
rename to core/work/src/main/kotlin/org/michaelbel/movies/work/di/WorkModule.kt
index c7b5fb4e3..2f6420f7f 100644
--- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/di/WorkManagerModule.kt
+++ b/core/work/src/main/kotlin/org/michaelbel/movies/work/di/WorkModule.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.domain.workers.di
+package org.michaelbel.movies.work.di
import android.content.Context
import androidx.work.WorkManager
@@ -7,15 +7,15 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
-internal object WorkManagerModule {
+internal object WorkModule {
@Provides
- @Singleton
fun provideWorkManager(
@ApplicationContext context: Context
- ): WorkManager = WorkManager.getInstance(context)
+ ): WorkManager {
+ return WorkManager.getInstance(context)
+ }
}
\ No newline at end of file
diff --git a/feature/account-impl/build.gradle.kts b/feature/account-impl/build.gradle.kts
index 789ac7e66..afdd17cc6 100644
--- a/feature/account-impl/build.gradle.kts
+++ b/feature/account-impl/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -55,8 +54,10 @@ dependencies {
api(project(":core:navigation"))
api(project(":core:ui"))
implementation(project(":core:common"))
- implementation(project(":core:domain"))
+ implementation(project(":core:interactor"))
implementation(project(":core:network"))
+ implementation(libs.androidx.work.runtime.ktx)
+ implementation(libs.androidx.hilt.work)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit.ktx)
diff --git a/feature/account-impl/src/main/kotlin/org/michaelbel/movies/account/ui/AccountScreenContent.kt b/feature/account-impl/src/main/kotlin/org/michaelbel/movies/account/ui/AccountScreenContent.kt
index 41f9ba320..f3ad45a7c 100644
--- a/feature/account-impl/src/main/kotlin/org/michaelbel/movies/account/ui/AccountScreenContent.kt
+++ b/feature/account-impl/src/main/kotlin/org/michaelbel/movies/account/ui/AccountScreenContent.kt
@@ -31,6 +31,7 @@ import org.michaelbel.movies.persistence.database.entity.AccountDb
import org.michaelbel.movies.persistence.database.ktx.orEmpty
import org.michaelbel.movies.ui.compose.AccountAvatar
import org.michaelbel.movies.ui.icons.MoviesIcons
+import org.michaelbel.movies.ui.ktx.isPortrait
import org.michaelbel.movies.ui.ktx.lettersTextFontSizeLarge
import org.michaelbel.movies.ui.preview.DevicePreviews
import org.michaelbel.movies.ui.theme.MoviesTheme
@@ -64,7 +65,7 @@ internal fun AccountScreenContent(
) {
ConstraintLayout(
modifier
- .padding(horizontal = 16.dp)
+ .padding(horizontal = if (isPortrait) 16.dp else 64.dp)
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.primaryContainer,
diff --git a/feature/account/build.gradle.kts b/feature/account/build.gradle.kts
index 9aed5f2b0..87497dd6a 100644
--- a/feature/account/build.gradle.kts
+++ b/feature/account/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
}
android {
diff --git a/feature/auth-impl/build.gradle.kts b/feature/auth-impl/build.gradle.kts
index 9cfa27b61..fae60a257 100644
--- a/feature/auth-impl/build.gradle.kts
+++ b/feature/auth-impl/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -52,10 +51,10 @@ android {
}
dependencies {
- api(project(":core:domain"))
api(project(":core:navigation"))
api(project(":core:ui"))
implementation(project(":core:common"))
+ implementation(project(":core:interactor"))
implementation(project(":core:network"))
implementation(libs.androidx.autofill)
diff --git a/feature/auth-impl/src/main/kotlin/org/michaelbel/movies/auth/ui/AuthScreenContent.kt b/feature/auth-impl/src/main/kotlin/org/michaelbel/movies/auth/ui/AuthScreenContent.kt
index 5353270f1..d37a7a7a8 100644
--- a/feature/auth-impl/src/main/kotlin/org/michaelbel/movies/auth/ui/AuthScreenContent.kt
+++ b/feature/auth-impl/src/main/kotlin/org/michaelbel/movies/auth/ui/AuthScreenContent.kt
@@ -5,13 +5,16 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
@@ -23,7 +26,6 @@ import androidx.compose.material3.TextButton
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.Modifier
@@ -46,13 +48,14 @@ import org.michaelbel.movies.auth.ktx.text
import org.michaelbel.movies.auth_impl.R
import org.michaelbel.movies.common.browser.openUrl
import org.michaelbel.movies.common.exceptions.CreateSessionWithLoginException
-import org.michaelbel.movies.entities.TMDB_PRIVACY_POLICY
-import org.michaelbel.movies.entities.TMDB_REGISTER
-import org.michaelbel.movies.entities.TMDB_RESET_PASSWORD
-import org.michaelbel.movies.entities.TMDB_TERMS_OF_USE
-import org.michaelbel.movies.entities.TMDB_URL
+import org.michaelbel.movies.network.TMDB_PRIVACY_POLICY
+import org.michaelbel.movies.network.TMDB_REGISTER
+import org.michaelbel.movies.network.TMDB_RESET_PASSWORD
+import org.michaelbel.movies.network.TMDB_TERMS_OF_USE
+import org.michaelbel.movies.network.TMDB_URL
import org.michaelbel.movies.ui.icons.MoviesIcons
import org.michaelbel.movies.ui.ktx.clickableWithoutRipple
+import org.michaelbel.movies.ui.ktx.isPortrait
@Composable
fun AuthRoute(
@@ -85,19 +88,21 @@ internal fun AuthScreenContent(
val toolbarColor: Int = MaterialTheme.colorScheme.primary.toArgb()
val focusManager: FocusManager = LocalFocusManager.current
+ val scrollState: ScrollState = rememberScrollState()
- var username: String by remember { mutableStateOf("") }
- var password: String by remember { mutableStateOf("") }
+ var username: String by rememberSaveable { mutableStateOf("") }
+ var password: String by rememberSaveable { mutableStateOf("") }
var passwordVisible: Boolean by rememberSaveable { mutableStateOf(false) }
ConstraintLayout(
modifier
- .padding(horizontal = 16.dp)
+ .padding(horizontal = if (isPortrait) 16.dp else 64.dp)
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.primaryContainer,
shape = MaterialTheme.shapes.small
)
+ .verticalScroll(scrollState)
) {
val (
toolbar,
@@ -276,7 +281,7 @@ internal fun AuthScreenContent(
Button(
onClick = {
- onSignInClick(username, password)
+ onSignInClick(username.trim(), password.trim())
},
modifier = Modifier
.constrainAs(signInButton) {
@@ -287,15 +292,11 @@ internal fun AuthScreenContent(
end.linkTo(parent.end, 16.dp)
},
enabled = username.isNotEmpty() && password.isNotEmpty() && !loading,
- contentPadding = PaddingValues(
- horizontal = 24.dp,
- vertical = 0.dp
- )
+ contentPadding = PaddingValues(horizontal = 24.dp)
) {
if (loading) {
CircularProgressIndicator(
- modifier = Modifier
- .size(24.dp),
+ modifier = Modifier.size(24.dp),
strokeWidth = 2.dp
)
} else {
diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts
index 9efd6ed6b..f4ce1a863 100644
--- a/feature/auth/build.gradle.kts
+++ b/feature/auth/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
}
android {
diff --git a/feature/details-impl/build.gradle.kts b/feature/details-impl/build.gradle.kts
index db9f37e77..8727b88c6 100644
--- a/feature/details-impl/build.gradle.kts
+++ b/feature/details-impl/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -55,7 +54,7 @@ dependencies {
api(project(":core:navigation"))
api(project(":core:ui"))
implementation(project(":core:common"))
- implementation(project(":core:domain"))
+ implementation(project(":core:interactor"))
implementation(project(":core:network"))
testImplementation(libs.junit)
diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/DetailsViewModel.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/DetailsViewModel.kt
index ce4c0c200..72771bcca 100644
--- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/DetailsViewModel.kt
+++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/DetailsViewModel.kt
@@ -2,6 +2,7 @@ package org.michaelbel.movies.details
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -10,16 +11,16 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.michaelbel.movies.common.ktx.require
import org.michaelbel.movies.common.viewmodel.BaseViewModel
-import org.michaelbel.movies.entities.lce.ScreenState
-import org.michaelbel.movies.interactor.usecase.MovieDetailsCase
+import org.michaelbel.movies.interactor.Interactor
+import org.michaelbel.movies.network.ScreenState
import org.michaelbel.movies.network.connectivity.NetworkManager
import org.michaelbel.movies.network.connectivity.NetworkStatus
-import javax.inject.Inject
+import org.michaelbel.movies.network.handle
@HiltViewModel
class DetailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
- private val movieDetails: MovieDetailsCase,
+ private val interactor: Interactor,
networkManager: NetworkManager
): BaseViewModel() {
@@ -42,6 +43,13 @@ class DetailsViewModel @Inject constructor(
fun retry() = loadMovie()
private fun loadMovie() = launch {
- _detailsState.tryEmit(movieDetails(movieId))
+ interactor.movieDetails(movieId).handle(
+ success = { movieDetailsData ->
+ _detailsState.emit(ScreenState.Content(movieDetailsData))
+ },
+ failure = { throwable ->
+ _detailsState.emit(ScreenState.Failure(throwable))
+ }
+ )
}
}
\ No newline at end of file
diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ktx/ScreenStateKtx.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ktx/ScreenStateKtx.kt
index a339e8f2a..9634ba38a 100644
--- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ktx/ScreenStateKtx.kt
+++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ktx/ScreenStateKtx.kt
@@ -3,7 +3,7 @@ package org.michaelbel.movies.details.ktx
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import org.michaelbel.movies.details_impl.R
-import org.michaelbel.movies.entities.lce.ScreenState
+import org.michaelbel.movies.network.ScreenState
import org.michaelbel.movies.persistence.database.entity.MovieDb
import org.michaelbel.movies.persistence.database.ktx.url
diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsContent.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsContent.kt
index 096acbf19..970bf7ddb 100644
--- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsContent.kt
+++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsContent.kt
@@ -27,6 +27,7 @@ import androidx.constraintlayout.compose.Dimension
import coil.compose.AsyncImage
import coil.request.ImageRequest
import org.michaelbel.movies.details_impl.R
+import org.michaelbel.movies.network.formatBackdropImage
import org.michaelbel.movies.persistence.database.entity.MovieDb
import org.michaelbel.movies.persistence.database.ktx.isNotEmpty
import org.michaelbel.movies.ui.ktx.context
@@ -57,7 +58,7 @@ fun DetailsContent(
null
} else {
ImageRequest.Builder(context)
- .data(movie.backdropPath)
+ .data(movie.backdropPath.formatBackdropImage)
.crossfade(true)
.build()
}
diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsScreenContent.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsScreenContent.kt
index b4c5c6f3f..62f39209e 100644
--- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsScreenContent.kt
+++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsScreenContent.kt
@@ -1,25 +1,30 @@
package org.michaelbel.movies.details.ui
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import java.net.UnknownHostException
import org.michaelbel.movies.details.DetailsViewModel
import org.michaelbel.movies.details.ktx.movie
import org.michaelbel.movies.details.ktx.movieUrl
import org.michaelbel.movies.details.ktx.toolbarTitle
-import org.michaelbel.movies.entities.lce.ScreenState
-import org.michaelbel.movies.entities.lce.ktx.isFailure
-import org.michaelbel.movies.entities.lce.ktx.throwable
+import org.michaelbel.movies.network.ScreenState
import org.michaelbel.movies.network.connectivity.NetworkStatus
import org.michaelbel.movies.network.connectivity.ktx.isAvailable
-import java.net.UnknownHostException
+import org.michaelbel.movies.network.ktx.isFailure
+import org.michaelbel.movies.network.ktx.throwable
+import org.michaelbel.movies.ui.ktx.displayCutoutWindowInsets
@Composable
fun DetailsRoute(
@@ -50,18 +55,22 @@ private fun DetailsScreenContent(
onRetry: () -> Unit,
modifier: Modifier = Modifier
) {
+ val topAppBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+
if (networkStatus.isAvailable && detailsState.isFailure && detailsState.throwable is UnknownHostException) {
onRetry()
}
Scaffold(
- modifier = modifier,
+ modifier = modifier
+ .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
topBar = {
DetailsToolbar(
movieTitle = detailsState.toolbarTitle,
movieUrl = detailsState.movieUrl,
onNavigationIconClick = onBackClick,
- modifier = Modifier.statusBarsPadding()
+ topAppBarScrollBehavior = topAppBarScrollBehavior,
+ modifier = Modifier.fillMaxWidth()
)
},
containerColor = MaterialTheme.colorScheme.primaryContainer
@@ -71,6 +80,7 @@ private fun DetailsScreenContent(
DetailsLoading(
modifier = Modifier
.padding(paddingValues)
+ .windowInsetsPadding(displayCutoutWindowInsets)
.fillMaxSize()
)
}
@@ -78,6 +88,7 @@ private fun DetailsScreenContent(
DetailsContent(
modifier = Modifier
.padding(paddingValues)
+ .windowInsetsPadding(displayCutoutWindowInsets)
.fillMaxSize(),
movie = detailsState.movie,
onNavigateToGallery = onNavigateToGallery
@@ -87,6 +98,7 @@ private fun DetailsScreenContent(
DetailsFailure(
modifier = Modifier
.padding(paddingValues)
+ .windowInsetsPadding(displayCutoutWindowInsets)
.fillMaxSize()
)
}
diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsToolbar.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsToolbar.kt
index 9e52fe9f5..b6becac0a 100644
--- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsToolbar.kt
+++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsToolbar.kt
@@ -4,18 +4,20 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import org.michaelbel.movies.ui.icons.MoviesIcons
+import org.michaelbel.movies.ui.ktx.displayCutoutWindowInsets
import org.michaelbel.movies.ui.preview.DevicePreviews
import org.michaelbel.movies.ui.preview.provider.TitlePreviewParameterProvider
import org.michaelbel.movies.ui.theme.MoviesTheme
@@ -25,6 +27,7 @@ fun DetailsToolbar(
movieTitle: String,
movieUrl: String?,
onNavigationIconClick: () -> Unit,
+ topAppBarScrollBehavior: TopAppBarScrollBehavior,
modifier: Modifier = Modifier
) {
TopAppBar(
@@ -42,6 +45,7 @@ fun DetailsToolbar(
actions = {
AnimatedVisibility(
visible = movieUrl != null,
+ modifier = Modifier.windowInsetsPadding(displayCutoutWindowInsets),
enter = fadeIn()
) {
if (movieUrl != null) {
@@ -53,9 +57,8 @@ fun DetailsToolbar(
},
navigationIcon = {
IconButton(
- onClick = {
- onNavigationIconClick()
- }
+ onClick = { onNavigationIconClick() },
+ modifier = Modifier.windowInsetsPadding(displayCutoutWindowInsets)
) {
Image(
imageVector = MoviesIcons.ArrowBack,
@@ -65,8 +68,10 @@ fun DetailsToolbar(
}
},
colors = TopAppBarDefaults.topAppBarColors(
- containerColor = Color.Transparent
- )
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ scrolledContainerColor = MaterialTheme.colorScheme.inversePrimary
+ ),
+ scrollBehavior = topAppBarScrollBehavior
)
}
@@ -80,6 +85,7 @@ private fun DetailsToolbarPreview(
movieTitle = title,
movieUrl = null,
onNavigationIconClick = {},
+ topAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
modifier = Modifier.statusBarsPadding()
)
}
diff --git a/feature/details/build.gradle.kts b/feature/details/build.gradle.kts
index b17a1b2f4..e7e56781b 100644
--- a/feature/details/build.gradle.kts
+++ b/feature/details/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
}
android {
diff --git a/feature/feed-impl/build.gradle.kts b/feature/feed-impl/build.gradle.kts
index 13935c106..4d587b182 100644
--- a/feature/feed-impl/build.gradle.kts
+++ b/feature/feed-impl/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -26,6 +25,7 @@ android {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.paging.ExperimentalPagingApi"
)
@@ -57,9 +57,10 @@ dependencies {
api(project(":core:navigation"))
api(project(":core:ui"))
implementation(project(":core:common"))
- implementation(project(":core:domain"))
+ implementation(project(":core:interactor"))
implementation(project(":core:network"))
implementation(project(":core:notifications"))
+ implementation(project(":core:persistence"))
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit.ktx)
diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/FeedViewModel.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/FeedViewModel.kt
index 168601103..d76353c31 100644
--- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/FeedViewModel.kt
+++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/FeedViewModel.kt
@@ -8,6 +8,7 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -22,8 +23,8 @@ import org.michaelbel.movies.common.appearance.FeedView
import org.michaelbel.movies.common.inappupdate.di.InAppUpdate
import org.michaelbel.movies.common.list.MovieList
import org.michaelbel.movies.common.viewmodel.BaseViewModel
-import org.michaelbel.movies.domain.mediator.MoviesRemoteMediator
import org.michaelbel.movies.feed.ktx.nameOrLocalList
+import org.michaelbel.movies.feed.remote.MoviesRemoteMediator
import org.michaelbel.movies.interactor.Interactor
import org.michaelbel.movies.network.connectivity.NetworkManager
import org.michaelbel.movies.network.connectivity.NetworkStatus
@@ -31,7 +32,6 @@ import org.michaelbel.movies.network.model.MovieResponse
import org.michaelbel.movies.notifications.NotificationClient
import org.michaelbel.movies.persistence.database.entity.AccountDb
import org.michaelbel.movies.persistence.database.entity.MovieDb
-import javax.inject.Inject
@HiltViewModel
class FeedViewModel @Inject constructor(
diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ktx/ConfigurationKtx.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ktx/ConfigurationKtx.kt
new file mode 100644
index 000000000..8ecf72d8f
--- /dev/null
+++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ktx/ConfigurationKtx.kt
@@ -0,0 +1,10 @@
+package org.michaelbel.movies.feed.ktx
+
+import androidx.compose.runtime.Composable
+import org.michaelbel.movies.ui.ktx.isPortrait
+
+private const val FEED_GRID_PORTRAIT_COLUMNS_COUNT = 2
+private const val FEED_GRID_LANDSCAPE_COLUMNS_COUNT = 4
+
+val gridColumnsCount: Int
+ @Composable get() = if (isPortrait) FEED_GRID_PORTRAIT_COLUMNS_COUNT else FEED_GRID_LANDSCAPE_COLUMNS_COUNT
\ No newline at end of file
diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ktx/MovieListKtx.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ktx/MovieListKtx.kt
index dafb6fa52..e9fda4ef9 100644
--- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ktx/MovieListKtx.kt
+++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ktx/MovieListKtx.kt
@@ -3,7 +3,7 @@ package org.michaelbel.movies.feed.ktx
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import org.michaelbel.movies.common.list.MovieList
-import org.michaelbel.movies.entities.isTmdbApiKeyEmpty
+import org.michaelbel.movies.network.isTmdbApiKeyEmpty
import org.michaelbel.movies.feed_impl.R
import org.michaelbel.movies.persistence.database.entity.MovieDb
diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/mediator/MoviesRemoteMediator.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/remote/MoviesRemoteMediator.kt
similarity index 97%
rename from core/domain/src/main/kotlin/org/michaelbel/movies/domain/mediator/MoviesRemoteMediator.kt
rename to feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/remote/MoviesRemoteMediator.kt
index 0db10b3f4..b4bcae766 100644
--- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/mediator/MoviesRemoteMediator.kt
+++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/remote/MoviesRemoteMediator.kt
@@ -1,4 +1,4 @@
-package org.michaelbel.movies.domain.mediator
+package org.michaelbel.movies.feed.remote
import androidx.paging.LoadType
import androidx.paging.PagingState
diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedCellLoading.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedCellLoading.kt
deleted file mode 100644
index d2b4f5eb6..000000000
--- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedCellLoading.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.michaelbel.movies.feed.ui
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import org.michaelbel.movies.network.model.MovieResponse
-import org.michaelbel.movies.persistence.database.entity.MovieDb
-import org.michaelbel.movies.ui.placeholder.PlaceholderHighlight
-import org.michaelbel.movies.ui.placeholder.material3.fade
-import org.michaelbel.movies.ui.placeholder.material3.placeholder
-import org.michaelbel.movies.ui.preview.DevicePreviews
-import org.michaelbel.movies.ui.theme.MoviesTheme
-
-@Composable
-fun FeedCellLoading(
- modifier: Modifier = Modifier,
- paddingValues: PaddingValues = PaddingValues(),
-) {
- LazyColumn(
- modifier = modifier,
- contentPadding = paddingValues
- ) {
- items(MovieResponse.DEFAULT_PAGE_SIZE) {
- FeedCellMovieBox(
- movie = MovieDb.Empty,
- modifier = Modifier
- .fillMaxWidth()
- .padding(
- horizontal = 8.dp,
- vertical = 4.dp
- )
- .placeholder(
- visible = true,
- color = MaterialTheme.colorScheme.inversePrimary,
- shape = MaterialTheme.shapes.small,
- highlight = PlaceholderHighlight.fade()
- )
- )
- }
- }
-}
-
-@Composable
-@DevicePreviews
-private fun FeedCellLoadingPreview() {
- MoviesTheme {
- FeedCellLoading(
- modifier = Modifier
- .fillMaxSize()
- .padding(top = 4.dp)
- .background(MaterialTheme.colorScheme.background)
- )
- }
-}
\ No newline at end of file
diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedCellMovieBox.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedCellMovieBox.kt
index 642c18805..51241934b 100644
--- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedCellMovieBox.kt
+++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedCellMovieBox.kt
@@ -24,6 +24,7 @@ import androidx.constraintlayout.compose.Dimension
import coil.compose.AsyncImage
import coil.request.ImageRequest
import org.michaelbel.movies.feed_impl.R
+import org.michaelbel.movies.network.formatBackdropImage
import org.michaelbel.movies.persistence.database.entity.MovieDb
import org.michaelbel.movies.ui.ktx.context
import org.michaelbel.movies.ui.ktx.isErrorOrEmpty
@@ -34,7 +35,8 @@ import org.michaelbel.movies.ui.theme.MoviesTheme
@Composable
fun FeedCellMovieBox(
movie: MovieDb,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ maxLines: Int = 10
) {
var isNoImageVisible: Boolean by remember { mutableStateOf(false) }
@@ -45,7 +47,7 @@ fun FeedCellMovieBox(
AsyncImage(
model = ImageRequest.Builder(context)
- .data(movie.backdropPath)
+ .data(movie.backdropPath.formatBackdropImage)
.crossfade(true)
.build(),
contentDescription = null,
@@ -95,7 +97,7 @@ fun FeedCellMovieBox(
end.linkTo(parent.end, 16.dp)
bottom.linkTo(parent.bottom, 16.dp)
},
- maxLines = 10,
+ maxLines = maxLines,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onPrimaryContainer
diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedContent.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedContent.kt
index 210de08bc..c937db40c 100644
--- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedContent.kt
+++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedContent.kt
@@ -9,6 +9,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
@@ -21,137 +24,246 @@ import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import org.michaelbel.movies.common.appearance.FeedView
-import org.michaelbel.movies.entities.isTmdbApiKeyEmpty
+import org.michaelbel.movies.feed.ktx.gridColumnsCount
+import org.michaelbel.movies.network.isTmdbApiKeyEmpty
import org.michaelbel.movies.persistence.database.entity.MovieDb
import org.michaelbel.movies.ui.ktx.isNotEmpty
import org.michaelbel.movies.ui.ktx.isPagingFailure
import org.michaelbel.movies.ui.ktx.isPagingLoading
+import org.michaelbel.movies.ui.ktx.isPortrait
@Composable
fun FeedContent(
- currentFeedView: FeedView,
+ feedView: FeedView,
lazyListState: LazyListState,
+ lazyGridState: LazyGridState,
lazyStaggeredGridState: LazyStaggeredGridState,
pagingItems: LazyPagingItems,
onMovieClick: (Int) -> Unit,
contentPadding: PaddingValues,
- modifier: Modifier = Modifier,
+ modifier: Modifier = Modifier
) {
- when (currentFeedView) {
+ when (feedView) {
is FeedView.FeedList -> {
- LazyColumn(
- modifier = modifier.padding(top = 4.dp),
- state = lazyListState,
- contentPadding = contentPadding
- ) {
- items(
- count = pagingItems.itemCount,
- key = pagingItems.itemKey(),
- contentType = pagingItems.itemContentType()
- ) { index ->
- val movieDb: MovieDb? = pagingItems[index]
- if (movieDb != null) {
- FeedCellMovieBox(
- movie = movieDb,
+ if (isPortrait) {
+ FeedContentColumn(
+ lazyListState = lazyListState,
+ pagingItems = pagingItems,
+ onMovieClick = onMovieClick,
+ contentPadding = contentPadding,
+ modifier = modifier
+ )
+ } else {
+ FeedContentGrid(
+ lazyGridState = lazyGridState,
+ pagingItems = pagingItems,
+ onMovieClick = onMovieClick,
+ contentPadding = contentPadding,
+ modifier = modifier
+ )
+ }
+ }
+ is FeedView.FeedGrid -> {
+ FeedContentStaggeredGrid(
+ lazyStaggeredGridState = lazyStaggeredGridState,
+ pagingItems = pagingItems,
+ onMovieClick = onMovieClick,
+ contentPadding = contentPadding,
+ modifier = modifier
+ )
+ }
+ }
+}
+
+@Composable
+private fun FeedContentColumn(
+ lazyListState: LazyListState,
+ pagingItems: LazyPagingItems,
+ onMovieClick: (Int) -> Unit,
+ contentPadding: PaddingValues,
+ modifier: Modifier = Modifier
+) {
+ LazyColumn(
+ modifier = modifier.padding(top = 4.dp),
+ state = lazyListState,
+ contentPadding = contentPadding
+ ) {
+ items(
+ count = pagingItems.itemCount,
+ key = pagingItems.itemKey(),
+ contentType = pagingItems.itemContentType()
+ ) { index ->
+ val movieDb: MovieDb? = pagingItems[index]
+ if (movieDb != null) {
+ FeedCellMovieBox(
+ movie = movieDb,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = 8.dp,
+ vertical = 4.dp
+ )
+ .clip(MaterialTheme.shapes.small)
+ .background(MaterialTheme.colorScheme.inversePrimary)
+ .clickable {
+ onMovieClick(movieDb.movieId)
+ }
+ )
+ }
+ }
+ if (isTmdbApiKeyEmpty && pagingItems.isNotEmpty) {
+ item {
+ FeedApiKeyBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp)
+ )
+ }
+ }
+ pagingItems.apply {
+ when {
+ isPagingLoading -> {
+ item {
+ FeedLoadingBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ )
+ }
+ }
+ isPagingFailure -> {
+ item {
+ FeedErrorBox(
modifier = Modifier
.fillMaxWidth()
- .padding(
- horizontal = 8.dp,
- vertical = 4.dp
- )
+ .height(80.dp)
+ .padding(start = 8.dp, top = 4.dp, end = 8.dp)
.clip(MaterialTheme.shapes.small)
- .background(MaterialTheme.colorScheme.inversePrimary)
- .clickable {
- onMovieClick(movieDb.movieId)
- }
+ .clickable { retry() }
)
}
}
- if (isTmdbApiKeyEmpty && pagingItems.isNotEmpty) {
+ }
+ }
+ }
+}
+
+@Composable
+private fun FeedContentGrid(
+ lazyGridState: LazyGridState,
+ pagingItems: LazyPagingItems,
+ onMovieClick: (Int) -> Unit,
+ contentPadding: PaddingValues,
+ modifier: Modifier = Modifier
+) {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ modifier = modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp),
+ state = lazyGridState,
+ contentPadding = contentPadding,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(
+ count = pagingItems.itemCount,
+ key = pagingItems.itemKey(),
+ contentType = pagingItems.itemContentType()
+ ) { index ->
+ val movieDb: MovieDb? = pagingItems[index]
+ if (movieDb != null) {
+ FeedCellMovieBox(
+ movie = movieDb,
+ maxLines = 1,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ .clip(MaterialTheme.shapes.small)
+ .background(MaterialTheme.colorScheme.inversePrimary)
+ .clickable { onMovieClick(movieDb.movieId) }
+ )
+ }
+ }
+ pagingItems.apply {
+ when {
+ isPagingLoading -> {
item {
- FeedApiKeyBox(
+ FeedLoadingBox(
modifier = Modifier
.fillMaxWidth()
- .padding(vertical = 16.dp)
+ .height(80.dp)
+
)
}
}
- pagingItems.apply {
- when {
- isPagingLoading -> {
- item {
- FeedLoadingBox(
- modifier = Modifier
- .fillMaxWidth()
- .height(80.dp)
- )
- }
- }
- isPagingFailure -> {
- item {
- FeedErrorBox(
- modifier = Modifier
- .fillMaxWidth()
- .height(80.dp)
- .padding(start = 8.dp, top = 4.dp, end = 8.dp)
- .clip(MaterialTheme.shapes.small)
- .clickable { retry() }
- )
- }
- }
+ isPagingFailure -> {
+ item {
+ FeedErrorBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ .clip(MaterialTheme.shapes.small)
+ .clickable { retry() }
+ )
}
}
}
}
- is FeedView.FeedGrid -> {
- LazyVerticalStaggeredGrid(
- columns = StaggeredGridCells.Fixed(2),
- modifier = modifier.padding(start = 8.dp, top = 8.dp, end = 8.dp),
- state = lazyStaggeredGridState,
- contentPadding = contentPadding,
- verticalItemSpacing = 8.dp,
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- items(
- count = pagingItems.itemCount,
- key = pagingItems.itemKey(),
- contentType = pagingItems.itemContentType()
- ) { index ->
- val movieDb: MovieDb? = pagingItems[index]
- if (movieDb != null) {
- FeedGridMovieBox(
- movie = movieDb,
+ }
+}
+
+@Composable
+private fun FeedContentStaggeredGrid(
+ lazyStaggeredGridState: LazyStaggeredGridState,
+ pagingItems: LazyPagingItems,
+ onMovieClick: (Int) -> Unit,
+ contentPadding: PaddingValues,
+ modifier: Modifier = Modifier
+) {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(gridColumnsCount),
+ modifier = modifier.padding(start = 8.dp, top = 8.dp, end = 8.dp),
+ state = lazyStaggeredGridState,
+ contentPadding = contentPadding,
+ verticalItemSpacing = 8.dp,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ items(
+ count = pagingItems.itemCount,
+ key = pagingItems.itemKey(),
+ contentType = pagingItems.itemContentType()
+ ) { index ->
+ val movieDb: MovieDb? = pagingItems[index]
+ if (movieDb != null) {
+ FeedGridMovieBox(
+ movie = movieDb,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.small)
+ .background(MaterialTheme.colorScheme.inversePrimary)
+ .clickable { onMovieClick(movieDb.movieId) }
+ )
+ }
+ }
+ pagingItems.apply {
+ when {
+ isPagingLoading -> {
+ item {
+ FeedLoadingBox(
modifier = Modifier
.fillMaxWidth()
- .clip(MaterialTheme.shapes.small)
- .background(MaterialTheme.colorScheme.inversePrimary)
- .clickable { onMovieClick(movieDb.movieId) }
+ .height(80.dp)
+
)
}
}
- pagingItems.apply {
- when {
- isPagingLoading -> {
- item {
- FeedLoadingBox(
- modifier = Modifier
- .fillMaxWidth()
- .height(80.dp)
-
- )
- }
- }
- isPagingFailure -> {
- item {
- FeedErrorBox(
- modifier = Modifier
- .fillMaxWidth()
- .height(80.dp)
- .clip(MaterialTheme.shapes.small)
- .clickable { retry() }
- )
- }
- }
+ isPagingFailure -> {
+ item {
+ FeedErrorBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ .clip(MaterialTheme.shapes.small)
+ .clickable { retry() }
+ )
}
}
}
diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedGridLoading.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedGridLoading.kt
deleted file mode 100644
index c983ab0d0..000000000
--- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedGridLoading.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.michaelbel.movies.feed.ui
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
-import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import org.michaelbel.movies.network.model.MovieResponse
-import org.michaelbel.movies.persistence.database.entity.MovieDb
-import org.michaelbel.movies.ui.placeholder.PlaceholderHighlight
-import org.michaelbel.movies.ui.placeholder.material3.fade
-import org.michaelbel.movies.ui.placeholder.placeholder
-import org.michaelbel.movies.ui.preview.DevicePreviews
-import org.michaelbel.movies.ui.theme.MoviesTheme
-
-@Composable
-fun FeedGridLoading(
- modifier: Modifier = Modifier,
- paddingValues: PaddingValues = PaddingValues(),
-) {
- LazyVerticalStaggeredGrid(
- columns = StaggeredGridCells.Fixed(2),
- modifier = modifier,
- contentPadding = paddingValues,
- verticalItemSpacing = 8.dp,
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- items(MovieResponse.DEFAULT_PAGE_SIZE) {
- FeedGridMovieBox(
- movie = MovieDb.Empty,
- modifier = Modifier
- .fillMaxWidth()
- .placeholder(
- visible = true,
- color = MaterialTheme.colorScheme.inversePrimary,
- shape = MaterialTheme.shapes.small,
- highlight = PlaceholderHighlight.fade()
- )
- )
- }
- }
-}
-
-@Composable
-@DevicePreviews
-private fun FeedGridLoadingPreview() {
- MoviesTheme {
- FeedGridLoading(
- modifier = Modifier
- .fillMaxSize()
- .padding(start = 8.dp, top = 8.dp, end = 8.dp)
- .background(MaterialTheme.colorScheme.background)
- )
- }
-}
\ No newline at end of file
diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedGridMovieBox.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedGridMovieBox.kt
index 3e2031125..73c21824f 100644
--- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedGridMovieBox.kt
+++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedGridMovieBox.kt
@@ -23,6 +23,7 @@ import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import coil.compose.AsyncImage
import coil.request.ImageRequest
+import org.michaelbel.movies.network.formatPosterImage
import org.michaelbel.movies.feed_impl.R
import org.michaelbel.movies.persistence.database.entity.MovieDb
import org.michaelbel.movies.ui.ktx.context
@@ -45,7 +46,7 @@ fun FeedGridMovieBox(
AsyncImage(
model = ImageRequest.Builder(context)
- .data(movie.posterPath)
+ .data(movie.posterPath.formatPosterImage)
.crossfade(true)
.build(),
contentDescription = null,
diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedLoading.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedLoading.kt
new file mode 100644
index 000000000..9c184844a
--- /dev/null
+++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedLoading.kt
@@ -0,0 +1,182 @@
+package org.michaelbel.movies.feed.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.michaelbel.movies.common.appearance.FeedView
+import org.michaelbel.movies.feed.ktx.gridColumnsCount
+import org.michaelbel.movies.network.model.MovieResponse
+import org.michaelbel.movies.persistence.database.entity.MovieDb
+import org.michaelbel.movies.ui.ktx.isPortrait
+import org.michaelbel.movies.ui.placeholder.PlaceholderHighlight
+import org.michaelbel.movies.ui.placeholder.material3.fade
+import org.michaelbel.movies.ui.placeholder.placeholder
+import org.michaelbel.movies.ui.preview.DeviceLandscapePreviews
+import org.michaelbel.movies.ui.preview.DevicePreviews
+import org.michaelbel.movies.ui.preview.DeviceUserPreviews
+import org.michaelbel.movies.ui.theme.MoviesTheme
+
+@Composable
+fun FeedLoading(
+ feedView: FeedView,
+ modifier: Modifier = Modifier,
+ paddingValues: PaddingValues = PaddingValues()
+) {
+ when (feedView) {
+ is FeedView.FeedList -> {
+ if (isPortrait) {
+ FeedLoadingColumn(
+ modifier = modifier,
+ paddingValues = paddingValues
+ )
+ } else {
+ FeedLoadingGrid(
+ modifier = modifier,
+ paddingValues = paddingValues
+ )
+ }
+ }
+ is FeedView.FeedGrid -> {
+ FeedLoadingStaggeredGrid(
+ modifier = modifier,
+ paddingValues = paddingValues
+ )
+ }
+ }
+}
+
+@Composable
+private fun FeedLoadingColumn(
+ modifier: Modifier = Modifier,
+ paddingValues: PaddingValues = PaddingValues()
+) {
+ LazyColumn(
+ modifier = modifier.padding(top = 4.dp),
+ contentPadding = paddingValues,
+ userScrollEnabled = false
+ ) {
+ items(MovieResponse.DEFAULT_PAGE_SIZE.div(2)) {
+ FeedCellMovieBox(
+ movie = MovieDb.Empty,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = 8.dp,
+ vertical = 4.dp
+ )
+ .placeholder(
+ visible = true,
+ color = MaterialTheme.colorScheme.inversePrimary,
+ shape = MaterialTheme.shapes.small,
+ highlight = PlaceholderHighlight.fade()
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun FeedLoadingGrid(
+ modifier: Modifier = Modifier,
+ paddingValues: PaddingValues = PaddingValues()
+) {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ modifier = modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp),
+ contentPadding = paddingValues,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ userScrollEnabled = false
+ ) {
+ items(MovieResponse.DEFAULT_PAGE_SIZE.div(2)) {
+ FeedCellMovieBox(
+ movie = MovieDb.Empty,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ .placeholder(
+ visible = true,
+ color = MaterialTheme.colorScheme.inversePrimary,
+ shape = MaterialTheme.shapes.small,
+ highlight = PlaceholderHighlight.fade()
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun FeedLoadingStaggeredGrid(
+ modifier: Modifier = Modifier,
+ paddingValues: PaddingValues = PaddingValues()
+) {
+ LazyVerticalStaggeredGrid(
+ columns = StaggeredGridCells.Fixed(gridColumnsCount),
+ modifier = modifier.padding(start = 8.dp, top = 8.dp, end = 8.dp),
+ contentPadding = paddingValues,
+ verticalItemSpacing = 8.dp,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ userScrollEnabled = false
+ ) {
+ items(MovieResponse.DEFAULT_PAGE_SIZE.div(2)) {
+ FeedGridMovieBox(
+ movie = MovieDb.Empty,
+ modifier = Modifier
+ .fillMaxWidth()
+ .placeholder(
+ visible = true,
+ color = MaterialTheme.colorScheme.inversePrimary,
+ shape = MaterialTheme.shapes.small,
+ highlight = PlaceholderHighlight.fade()
+ )
+ )
+ }
+ }
+}
+
+@Composable
+@DevicePreviews
+private fun FeedLoadingColumnPreview() {
+ MoviesTheme {
+ FeedLoadingColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ )
+ }
+}
+
+@Composable
+@DeviceLandscapePreviews
+private fun FeedLoadingGridPreview() {
+ MoviesTheme {
+ FeedLoadingGrid(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ )
+ }
+}
+
+@Composable
+@DeviceUserPreviews
+private fun FeedLoadingStaggeredGridPreview() {
+ MoviesTheme {
+ FeedLoadingStaggeredGrid(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedScreenContent.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedScreenContent.kt
index 32a9564b6..c17ce4c1a 100644
--- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedScreenContent.kt
+++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedScreenContent.kt
@@ -10,7 +10,10 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
@@ -27,9 +30,11 @@ import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material3.rememberStandardBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
@@ -39,25 +44,26 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
+import java.net.UnknownHostException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.michaelbel.movies.common.appearance.FeedView
import org.michaelbel.movies.common.exceptions.ApiKeyNotNullException
import org.michaelbel.movies.common.list.MovieList
-import org.michaelbel.movies.entities.isTmdbApiKeyEmpty
import org.michaelbel.movies.feed.FeedViewModel
import org.michaelbel.movies.feed.ktx.titleText
import org.michaelbel.movies.feed_impl.R
import org.michaelbel.movies.network.connectivity.NetworkStatus
+import org.michaelbel.movies.network.isTmdbApiKeyEmpty
import org.michaelbel.movies.persistence.database.entity.AccountDb
import org.michaelbel.movies.persistence.database.entity.MovieDb
import org.michaelbel.movies.persistence.database.ktx.orEmpty
import org.michaelbel.movies.ui.compose.NotificationBottomSheet
import org.michaelbel.movies.ui.ktx.clickableWithoutRipple
+import org.michaelbel.movies.ui.ktx.displayCutoutWindowInsets
import org.michaelbel.movies.ui.ktx.isFailure
import org.michaelbel.movies.ui.ktx.isLoading
import org.michaelbel.movies.ui.ktx.throwable
-import java.net.UnknownHostException
@Composable
fun FeedRoute(
@@ -115,6 +121,7 @@ private fun FeedScreenContent(
val context: Context = LocalContext.current
val scope: CoroutineScope = rememberCoroutineScope()
val lazyListState: LazyListState = rememberLazyListState()
+ val lazyGridState: LazyGridState = rememberLazyGridState()
val lazyStaggeredGridState: LazyStaggeredGridState = rememberLazyStaggeredGridState()
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
val notificationBottomSheetScaffoldState = rememberBottomSheetScaffoldState(
@@ -144,6 +151,9 @@ private fun FeedScreenContent(
scope.launch {
lazyListState.animateScrollToItem(0)
}
+ scope.launch {
+ lazyGridState.animateScrollToItem(0)
+ }
scope.launch {
lazyStaggeredGridState.animateScrollToItem(0)
}
@@ -174,9 +184,10 @@ private fun FeedScreenContent(
onNotificationBottomSheetShow()
}
- BackHandler(enabled = notificationBottomSheetScaffoldState.bottomSheetState.isVisible) {
- onNotificationBottomSheetHide()
- }
+ var isBottomSheetExpanded: Boolean by remember { mutableStateOf(false) }
+ isBottomSheetExpanded = notificationBottomSheetScaffoldState.bottomSheetState.currentValue == SheetValue.Expanded
+
+ BackHandler(isBottomSheetExpanded, onNotificationBottomSheetHide)
BottomSheetScaffold(
sheetContent = {
@@ -197,9 +208,7 @@ private fun FeedScreenContent(
topBar = {
FeedToolbar(
title = currentMovieList.titleText,
- modifier = Modifier
- .fillMaxWidth()
- .clickableWithoutRipple { onScrollToTop() },
+ modifier = Modifier.clickableWithoutRipple { onScrollToTop() },
account = account,
isUpdateIconVisible = isUpdateIconVisible,
onAuthIconClick = {
@@ -223,27 +232,17 @@ private fun FeedScreenContent(
) { paddingValues ->
when {
pagingItems.isLoading -> {
- when (currentFeedView) {
- is FeedView.FeedList -> {
- FeedCellLoading(
- modifier = Modifier.fillMaxSize().padding(top = 4.dp),
- paddingValues = paddingValues
- )
- }
- is FeedView.FeedGrid -> {
- FeedGridLoading(
- modifier = Modifier
- .fillMaxSize()
- .padding(start = 8.dp, top = 8.dp, end = 8.dp),
- paddingValues = paddingValues
- )
- }
- }
+ FeedLoading(
+ feedView = currentFeedView,
+ modifier = Modifier.windowInsetsPadding(displayCutoutWindowInsets),
+ paddingValues = paddingValues
+ )
}
pagingItems.isFailure -> {
FeedFailure(
modifier = Modifier
.padding(paddingValues)
+ .windowInsetsPadding(displayCutoutWindowInsets)
.fillMaxSize()
.clickableWithoutRipple { pagingItems.retry() },
onCheckConnectivityClick = {
@@ -257,13 +256,14 @@ private fun FeedScreenContent(
}
else -> {
FeedContent(
- currentFeedView = currentFeedView,
+ feedView = currentFeedView,
lazyListState = lazyListState,
+ lazyGridState = lazyGridState,
lazyStaggeredGridState = lazyStaggeredGridState,
pagingItems = pagingItems,
onMovieClick = onNavigateToDetails,
contentPadding = paddingValues,
- modifier = Modifier.fillMaxSize()
+ modifier = Modifier.windowInsetsPadding(displayCutoutWindowInsets)
)
}
}
diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedToolbar.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedToolbar.kt
index d047e021f..9268b57f3 100644
--- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedToolbar.kt
+++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedToolbar.kt
@@ -1,8 +1,10 @@
package org.michaelbel.movies.feed.ui
import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -21,6 +23,7 @@ import org.michaelbel.movies.persistence.database.entity.AccountDb
import org.michaelbel.movies.persistence.database.ktx.isEmpty
import org.michaelbel.movies.ui.compose.AccountAvatar
import org.michaelbel.movies.ui.icons.MoviesIcons
+import org.michaelbel.movies.ui.ktx.displayCutoutWindowInsets
import org.michaelbel.movies.ui.ktx.lettersTextFontSizeSmall
import org.michaelbel.movies.ui.preview.DevicePreviews
import org.michaelbel.movies.ui.preview.provider.BooleanPreviewParameterProvider
@@ -42,6 +45,7 @@ fun FeedToolbar(
title = {
Text(
text = title,
+ modifier = Modifier.windowInsetsPadding(displayCutoutWindowInsets),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.titleLarge.copy(
@@ -51,43 +55,47 @@ fun FeedToolbar(
},
modifier = modifier,
actions = {
- if (isUpdateIconVisible) {
+ Row(
+ modifier = Modifier.windowInsetsPadding(displayCutoutWindowInsets)
+ ) {
+ if (isUpdateIconVisible) {
+ IconButton(
+ onClick = onUpdateIconClick
+ ) {
+ Image(
+ imageVector = MoviesIcons.SystemUpdate,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer)
+ )
+ }
+ }
+
IconButton(
- onClick = onUpdateIconClick
+ onClick = onSettingsIconClick
) {
Image(
- imageVector = MoviesIcons.SystemUpdate,
+ imageVector = MoviesIcons.Settings,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer)
)
}
- }
-
- IconButton(
- onClick = onSettingsIconClick
- ) {
- Image(
- imageVector = MoviesIcons.Settings,
- contentDescription = null,
- colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer)
- )
- }
- IconButton(
- onClick = if (account.isEmpty) onAuthIconClick else onAccountIconClick
- ) {
- if (account.isEmpty) {
- Image(
- imageVector = MoviesIcons.Account,
- contentDescription = null,
- colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer)
- )
- } else {
- AccountAvatar(
- account = account,
- fontSize = account.lettersTextFontSizeSmall,
- modifier = Modifier.size(32.dp)
- )
+ IconButton(
+ onClick = if (account.isEmpty) onAuthIconClick else onAccountIconClick
+ ) {
+ if (account.isEmpty) {
+ Image(
+ imageVector = MoviesIcons.Account,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer)
+ )
+ } else {
+ AccountAvatar(
+ account = account,
+ fontSize = account.lettersTextFontSizeSmall,
+ modifier = Modifier.size(32.dp)
+ )
+ }
}
}
},
diff --git a/feature/feed/build.gradle.kts b/feature/feed/build.gradle.kts
index bb26112af..03fd62d3e 100644
--- a/feature/feed/build.gradle.kts
+++ b/feature/feed/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
}
android {
diff --git a/feature/gallery-impl/build.gradle.kts b/feature/gallery-impl/build.gradle.kts
index 5bc60c3ae..62704f3b9 100644
--- a/feature/gallery-impl/build.gradle.kts
+++ b/feature/gallery-impl/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -58,8 +57,9 @@ dependencies {
api(project(":core:navigation"))
api(project(":core:ui"))
implementation(project(":core:common"))
- implementation(project(":core:domain"))
+ implementation(project(":core:interactor"))
implementation(project(":core:network"))
+ implementation(project(":core:work"))
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit.ktx)
diff --git a/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/GalleryViewModel.kt b/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/GalleryViewModel.kt
index 44aca68ac..1370536de 100644
--- a/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/GalleryViewModel.kt
+++ b/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/GalleryViewModel.kt
@@ -1,35 +1,78 @@
package org.michaelbel.movies.gallery
import androidx.lifecycle.SavedStateHandle
+import androidx.work.Constraints
+import androidx.work.Data
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.michaelbel.movies.common.ktx.require
import org.michaelbel.movies.common.viewmodel.BaseViewModel
+import org.michaelbel.movies.gallery.ktx.nameRes
+import org.michaelbel.movies.gallery_impl.R
import org.michaelbel.movies.interactor.Interactor
-import org.michaelbel.movies.network.model.ImagesResponse
+import org.michaelbel.movies.persistence.database.entity.ImageDb
+import org.michaelbel.movies.persistence.database.ktx.original
+import org.michaelbel.movies.work.DownloadImageWorker
@HiltViewModel
class GalleryViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
- private val interactor: Interactor
+ private val interactor: Interactor,
+ private val workManager: WorkManager
): BaseViewModel() {
private val movieId: String = savedStateHandle.require("movieId")
- val imageFlow: StateFlow = interactor.movieImage(movieId.toInt())
+ val movieImagesFlow: StateFlow> = interactor.imagesFlow(movieId.toInt())
.stateIn(
scope = this,
started = SharingStarted.Lazily,
- initialValue = ""
+ initialValue = emptyList()
)
+ private val _workInfoFlow: MutableStateFlow = MutableStateFlow(null)
+ val workInfoFlow: StateFlow = _workInfoFlow.asStateFlow()
+
init {
- launch {
- val imagesResponse: ImagesResponse = interactor.movieImages(movieId.toInt())
+ loadMovieImages(movieId.toInt())
+ }
+
+ fun downloadImage(imageDb: ImageDb) = launch {
+ val workData = Data.Builder()
+ .putString(DownloadImageWorker.KEY_IMAGE_URL, imageDb.original)
+ .putInt(DownloadImageWorker.KEY_CONTENT_TITLE, R.string.gallery_downloading_image)
+ .putInt(DownloadImageWorker.KEY_CONTENT_TEXT, imageDb.type.nameRes)
+ .build()
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .setRequiresStorageNotLow(true)
+ .setRequiresBatteryNotLow(true)
+ .build()
+ val downloadImageWorker = OneTimeWorkRequestBuilder()
+ .addTag(DownloadImageWorker.DOWNLOAD_IMAGE_WORKER_TAG)
+ .setConstraints(constraints)
+ .setInputData(workData)
+ .build()
+ workManager.run {
+ enqueueUniqueWork(imageDb.toString(), ExistingWorkPolicy.KEEP, downloadImageWorker)
+ getWorkInfoByIdFlow(downloadImageWorker.id).collect { workInfo ->
+ _workInfoFlow.emit(workInfo)
+ }
}
}
+
+ private fun loadMovieImages(movieId: Int) = launch {
+ interactor.images(movieId)
+ }
}
\ No newline at end of file
diff --git a/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/ktx/ImageTypeKtx.kt b/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/ktx/ImageTypeKtx.kt
new file mode 100644
index 000000000..52aed62de
--- /dev/null
+++ b/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/ktx/ImageTypeKtx.kt
@@ -0,0 +1,12 @@
+package org.michaelbel.movies.gallery.ktx
+
+import androidx.annotation.StringRes
+import org.michaelbel.movies.gallery_impl.R
+import org.michaelbel.movies.persistence.database.entity.ImageDb
+
+internal val ImageDb.Type.nameRes: Int
+ @StringRes get() = when (this) {
+ ImageDb.Type.POSTER -> R.string.gallery_poster
+ ImageDb.Type.BACKDROP -> R.string.gallery_backdrop
+ ImageDb.Type.LOGO -> R.string.gallery_logo
+ }
\ No newline at end of file
diff --git a/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/ui/GalleryLoading.kt b/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/ui/GalleryLoading.kt
new file mode 100644
index 000000000..d29b01a62
--- /dev/null
+++ b/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/ui/GalleryLoading.kt
@@ -0,0 +1,36 @@
+package org.michaelbel.movies.gallery.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import org.michaelbel.movies.ui.preview.DevicePreviews
+import org.michaelbel.movies.ui.theme.MoviesTheme
+
+@Composable
+fun GalleryLoading(
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center
+ ) {
+ LinearProgressIndicator(
+ modifier = Modifier,
+ trackColor = MaterialTheme.colorScheme.inversePrimary
+ )
+ }
+}
+
+@Composable
+@DevicePreviews
+private fun GalleryLoadingPreview() {
+ MoviesTheme {
+ GalleryLoading(
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/ui/GalleryScreenContent.kt b/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/ui/GalleryScreenContent.kt
index df22d84be..5a4262b82 100644
--- a/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/ui/GalleryScreenContent.kt
+++ b/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/ui/GalleryScreenContent.kt
@@ -1,43 +1,69 @@
package org.michaelbel.movies.gallery.ui
-import android.content.Context
+import android.content.Intent
+import android.net.Uri
import androidx.activity.compose.BackHandler
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerDefaults
import androidx.compose.foundation.pager.PagerScope
+import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.IconButton
+import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+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.rememberCoroutineScope
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
+import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.work.WorkInfo
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.request.ImageRequest
import kotlinx.coroutines.launch
-import org.michaelbel.movies.entities.image.original
import org.michaelbel.movies.gallery.GalleryViewModel
import org.michaelbel.movies.gallery.zoomable.rememberZoomState
import org.michaelbel.movies.gallery.zoomable.zoomable
+import org.michaelbel.movies.gallery_impl.R
+import org.michaelbel.movies.network.isNotOriginal
+import org.michaelbel.movies.persistence.database.entity.ImageDb
+import org.michaelbel.movies.persistence.database.ktx.original
import org.michaelbel.movies.ui.icons.MoviesIcons
+import org.michaelbel.movies.ui.ktx.displayCutoutWindowInsets
+import org.michaelbel.movies.work.DownloadImageWorker
@Composable
fun GalleryRoute(
@@ -45,123 +71,280 @@ fun GalleryRoute(
modifier: Modifier = Modifier,
viewModel: GalleryViewModel = hiltViewModel()
) {
- val movieImage: String by viewModel.imageFlow.collectAsStateWithLifecycle()
+ val movieImages: List by viewModel.movieImagesFlow.collectAsStateWithLifecycle()
+ val workInfo: WorkInfo? by viewModel.workInfoFlow.collectAsStateWithLifecycle()
GalleryScreenContent(
- movieImage = movieImage,
+ movieImages = movieImages,
+ workInfo = workInfo,
onBackClick = onBackClick,
+ onDownloadClick = viewModel::downloadImage,
modifier = modifier
)
}
@Composable
private fun GalleryScreenContent(
- movieImage: String,
+ movieImages: List,
+ workInfo: WorkInfo?,
onBackClick: () -> Unit,
+ onDownloadClick: (ImageDb) -> Unit,
modifier: Modifier = Modifier
) {
- val context: Context = LocalContext.current
+ val context = LocalContext.current
+ val hapticFeedback = LocalHapticFeedback.current
val coroutineScope = rememberCoroutineScope()
+ val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
+ val resultContract = rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {}
- var imageDiskCacheKey: String? by remember { mutableStateOf(null) }
- var image: String by remember { mutableStateOf("") }
- image = movieImage
+ val onSuccessSnackbar: (String, String, Uri) -> Unit = { message, actionLabel, uri ->
+ coroutineScope.launch {
+ val result = snackbarHostState.showSnackbar(
+ message = message,
+ actionLabel = actionLabel,
+ duration = SnackbarDuration.Long
+ )
+ if (result == SnackbarResult.ActionPerformed) {
+ Intent(Intent.ACTION_VIEW).apply {
+ setDataAndType(uri, "image/jpg")
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }.also { intent ->
+ resultContract.launch(intent)
+ }
+ }
+ }
+ }
+ val onFailureSnackbar: (String) -> Unit = { message ->
+ coroutineScope.launch {
+ snackbarHostState.showSnackbar(
+ message = message,
+ duration = SnackbarDuration.Short
+ )
+ }
+ }
- ConstraintLayout(
- modifier = modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.primaryContainer)
- ) {
- val (pager, backIcon) = createRefs()
-
- LoopHorizontalPager(
- count = 1,
- modifier = Modifier.constrainAs(pager) {
- width = Dimension.fillToConstraints
- height = Dimension.wrapContent
- start.linkTo(parent.start)
- top.linkTo(parent.top)
- end.linkTo(parent.end)
- bottom.linkTo(parent.bottom)
+ when (workInfo?.state) {
+ WorkInfo.State.SUCCEEDED -> {
+ val result: String = workInfo.outputData.getString(DownloadImageWorker.KEY_IMAGE_URL).orEmpty()
+ onSuccessSnackbar(
+ stringResource(R.string.gallery_success),
+ stringResource(R.string.gallery_action_open),
+ result.toUri()
+ )
+ }
+ WorkInfo.State.FAILED -> {
+ val result: String = workInfo.outputData.getString(DownloadImageWorker.KEY_IMAGE_URL).orEmpty()
+ if (result == DownloadImageWorker.FAILURE_RESULT) {
+ onFailureSnackbar(stringResource(R.string.gallery_failure))
}
- ) {
- Box(
- contentAlignment = Alignment.Center
- ) {
- val zoomState = rememberZoomState()
-
- AsyncImage(
- model = ImageRequest.Builder(context)
- .data(image)
- .crossfade(true)
- .placeholderMemoryCacheKey(imageDiskCacheKey)
- .build(),
- contentDescription = null,
- modifier = Modifier
- .fillMaxSize()
- .zoomable(zoomState),
- transform = { state ->
- if (state is AsyncImagePainter.State.Success) {
- zoomState.setContentSize(state.painter.intrinsicSize)
- imageDiskCacheKey = state.result.diskCacheKey
- if (image != image.original) {
- image = image.original
+ }
+ else -> {}
+ }
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ snackbarHost = {
+ SnackbarHost(hostState = snackbarHostState)
+ },
+ containerColor = MaterialTheme.colorScheme.primaryContainer
+ ) { paddingValues ->
+ when {
+ movieImages.isEmpty() -> {
+ GalleryLoading(
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ else -> {
+ ConstraintLayout(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ val (pager, backIcon, title) = createRefs()
+
+ val initialPage = 0
+ val pagerState = rememberPagerState(
+ initialPage = initialPage,
+ initialPageOffsetFraction = 0F,
+ pageCount = { movieImages.size }
+ )
+
+ var currentPage: Int by remember { mutableStateOf(0) }
+ LaunchedEffect(pagerState) {
+ snapshotFlow { pagerState.currentPage }.collect { page ->
+ if (currentPage != page) {
+ hapticFeedback.performHapticFeedback(hapticFeedbackType = HapticFeedbackType.LongPress)
+ currentPage = page
}
}
- state
- },
- contentScale = ContentScale.Fit
- )
+ }
+
+ LoopHorizontalPager(
+ pagerState = pagerState,
+ modifier = Modifier.constrainAs(pager) {
+ width = Dimension.fillToConstraints
+ height = Dimension.wrapContent
+ start.linkTo(parent.start)
+ top.linkTo(parent.top)
+ end.linkTo(parent.end)
+ bottom.linkTo(parent.bottom)
+ }
+ ) { page ->
+ currentPage = page
+
+ val imageDb: ImageDb = movieImages[page]
+ var imageDiskCacheKey: String? by remember { mutableStateOf(null) }
+
+ var image: String by remember { mutableStateOf("") }
+ image = imageDb.original
- BackHandler(zoomState.isScaled) {
- coroutineScope.launch { zoomState.reset() }
+ var loading: Boolean by remember { mutableStateOf(true) }
+
+ ConstraintLayout(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ val (asyncImage, progressBar, downloadIcon) = createRefs()
+
+ val zoomState = rememberZoomState()
+
+ AsyncImage(
+ model = ImageRequest.Builder(context)
+ .data(image)
+ .crossfade(true)
+ .placeholderMemoryCacheKey(imageDiskCacheKey)
+ .build(),
+ contentDescription = null,
+ modifier = Modifier
+ .constrainAs(asyncImage) {
+ width = Dimension.fillToConstraints
+ height = Dimension.fillToConstraints
+ start.linkTo(parent.start)
+ top.linkTo(parent.top)
+ end.linkTo(parent.end)
+ bottom.linkTo(parent.bottom)
+ }
+ .zoomable(zoomState),
+ transform = { state ->
+ loading = state is AsyncImagePainter.State.Loading
+
+ if (state is AsyncImagePainter.State.Success) {
+ zoomState.setContentSize(state.painter.intrinsicSize)
+ imageDiskCacheKey = state.result.diskCacheKey
+ if (image.isNotOriginal) {
+ image = imageDb.original
+ }
+ }
+
+ state
+ },
+ contentScale = ContentScale.Fit
+ )
+
+ if (loading) {
+ LinearProgressIndicator(
+ modifier = Modifier
+ .constrainAs(progressBar) {
+ width = Dimension.wrapContent
+ height = Dimension.wrapContent
+ start.linkTo(parent.start)
+ top.linkTo(parent.top)
+ end.linkTo(parent.end)
+ bottom.linkTo(parent.bottom)
+ }
+ .zoomable(zoomState),
+ trackColor = MaterialTheme.colorScheme.inversePrimary
+ )
+ }
+
+ AnimatedVisibility(
+ visible = !loading,
+ modifier = Modifier
+ .constrainAs(downloadIcon) {
+ width = Dimension.wrapContent
+ height = Dimension.wrapContent
+ end.linkTo(parent.end, 4.dp)
+ top.linkTo(parent.top, 8.dp)
+ }
+ .statusBarsPadding(),
+ enter = fadeIn()
+ ) {
+ IconButton(
+ onClick = { onDownloadClick(imageDb) },
+ modifier = Modifier.windowInsetsPadding(displayCutoutWindowInsets)
+ ) {
+ Image(
+ imageVector = MoviesIcons.FileDownload,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer)
+ )
+ }
+ }
+
+ BackHandler(zoomState.isScaled) {
+ coroutineScope.launch { zoomState.reset() }
+ }
+ }
+ }
+
+ IconButton(
+ onClick = onBackClick,
+ modifier = Modifier
+ .constrainAs(backIcon) {
+ width = Dimension.wrapContent
+ height = Dimension.wrapContent
+ start.linkTo(parent.start, 4.dp)
+ top.linkTo(parent.top, 8.dp)
+ }
+ .statusBarsPadding()
+ .windowInsetsPadding(displayCutoutWindowInsets)
+ ) {
+ Image(
+ imageVector = MoviesIcons.ArrowBack,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer)
+ )
+ }
+
+ Text(
+ text = "",
+ modifier = Modifier
+ .constrainAs(title) {
+ width = Dimension.wrapContent
+ height = Dimension.wrapContent
+ start.linkTo(backIcon.end, 4.dp)
+ top.linkTo(backIcon.top)
+ end.linkTo(parent.end, 4.dp)
+ bottom.linkTo(backIcon.bottom)
+ }
+ .statusBarsPadding(),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ textAlign = TextAlign.Start,
+ style = MaterialTheme.typography.titleLarge.copy(
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ )
}
}
}
-
- IconButton(
- onClick = onBackClick,
- modifier = Modifier.constrainAs(backIcon) {
- width = Dimension.wrapContent
- height = Dimension.wrapContent
- start.linkTo(parent.start, 4.dp)
- top.linkTo(parent.top, 8.dp)
- }.statusBarsPadding()
- ) {
- Image(
- imageVector = MoviesIcons.ArrowBack,
- contentDescription = null,
- colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer)
- )
- }
}
}
@Composable
private fun LoopHorizontalPager(
- count: Int,
+ pagerState: PagerState,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
content: @Composable PagerScope.(page: Int) -> Unit,
) {
- val startIndex: Int = Int.MAX_VALUE / 2
- val pagerState = rememberPagerState(
- initialPage = startIndex,
- initialPageOffsetFraction = 0F,
- pageCount = { count }
- )
HorizontalPager(
state = pagerState,
modifier = modifier,
contentPadding = contentPadding,
+ pageSpacing = 8.dp,
+ flingBehavior = PagerDefaults.flingBehavior(state = pagerState),
pageContent = { index ->
- val page = (index - startIndex).floorMod(count)
- content(page)
+ content(index)
}
)
-}
-
-private fun Int.floorMod(other: Int): Int = when (other) {
- 0 -> this
- else -> this - floorDiv(other) * other
}
\ No newline at end of file
diff --git a/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/zoomable/ZoomState.kt b/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/zoomable/ZoomState.kt
index 8a0901b6a..580a8393f 100644
--- a/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/zoomable/ZoomState.kt
+++ b/feature/gallery-impl/src/main/kotlin/org/michaelbel/movies/gallery/zoomable/ZoomState.kt
@@ -32,12 +32,12 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.input.pointer.util.VelocityTracker
-import java.lang.Float.max
-import kotlin.math.abs
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
+import java.lang.Float.max
+import kotlin.math.abs
/**
* A state object that manage scale and offset.
@@ -295,7 +295,6 @@ class ZoomState(
}
suspend fun endGesture() = coroutineScope {
- Log.e("2", "endGesture")
val velocity = velocityTracker.calculateVelocity()
if (velocity.x != 0f) {
launch {
diff --git a/feature/gallery-impl/src/main/res/values-ru/strings.xml b/feature/gallery-impl/src/main/res/values-ru/strings.xml
index 55344e519..4b305f97f 100644
--- a/feature/gallery-impl/src/main/res/values-ru/strings.xml
+++ b/feature/gallery-impl/src/main/res/values-ru/strings.xml
@@ -1,3 +1,10 @@
+ Загрузка изображения...
+ Постер
+ Обложка
+ Логотип
+ Загружено
+ Ошибка при загрузке
+ Открыть
\ No newline at end of file
diff --git a/feature/gallery-impl/src/main/res/values/strings.xml b/feature/gallery-impl/src/main/res/values/strings.xml
index 55344e519..1e6dfc3b3 100644
--- a/feature/gallery-impl/src/main/res/values/strings.xml
+++ b/feature/gallery-impl/src/main/res/values/strings.xml
@@ -1,3 +1,10 @@
+ Downloading Image...
+ Poster
+ Backdrop
+ Logo
+ Success
+ Failure
+ Show
\ No newline at end of file
diff --git a/feature/gallery/build.gradle.kts b/feature/gallery/build.gradle.kts
index 88a2c0734..9496764a9 100644
--- a/feature/gallery/build.gradle.kts
+++ b/feature/gallery/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
}
android {
diff --git a/feature/settings-impl/build.gradle.kts b/feature/settings-impl/build.gradle.kts
index 061a7c399..3d134f836 100644
--- a/feature/settings-impl/build.gradle.kts
+++ b/feature/settings-impl/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
id("movies-android-hilt")
}
@@ -56,7 +55,7 @@ dependencies {
api(project(":core:navigation"))
api(project(":core:common"))
api(project(":core:ui"))
- implementation(project(":core:domain"))
+ implementation(project(":core:interactor"))
implementation(project(":core:notifications"))
testImplementation(libs.junit)
diff --git a/feature/settings-impl/src/androidTest/kotlin/org/michaelbel/movies/settings/ui/SettingsVersionBoxTest.kt b/feature/settings-impl/src/androidTest/kotlin/org/michaelbel/movies/settings/ui/SettingsVersionBoxTest.kt
index 5ca7e95aa..87f7a88d7 100644
--- a/feature/settings-impl/src/androidTest/kotlin/org/michaelbel/movies/settings/ui/SettingsVersionBoxTest.kt
+++ b/feature/settings-impl/src/androidTest/kotlin/org/michaelbel/movies/settings/ui/SettingsVersionBoxTest.kt
@@ -53,6 +53,10 @@ internal class SettingsVersionBoxTest {
}
private companion object {
- private val APP_VERSION_DATA: AppVersionData = AppVersionData("1.0.0", 1L)
+ private val APP_VERSION_DATA: AppVersionData = AppVersionData(
+ version = "1.0.0",
+ code = 1L,
+ isDebug = true
+ )
}
}
\ No newline at end of file
diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/SettingsViewModel.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/SettingsViewModel.kt
index 05248d38a..e3b289ff4 100644
--- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/SettingsViewModel.kt
+++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/SettingsViewModel.kt
@@ -4,31 +4,25 @@ import android.os.Build
import androidx.compose.ui.unit.LayoutDirection
import androidx.lifecycle.DefaultLifecycleObserver
import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.michaelbel.movies.common.appearance.FeedView
import org.michaelbel.movies.common.list.MovieList
+import org.michaelbel.movies.common.localization.LocaleController
import org.michaelbel.movies.common.localization.model.AppLanguage
import org.michaelbel.movies.common.theme.AppTheme
import org.michaelbel.movies.common.version.AppVersionData
import org.michaelbel.movies.common.viewmodel.BaseViewModel
import org.michaelbel.movies.interactor.Interactor
import org.michaelbel.movies.interactor.usecase.DelayUseCase
-import org.michaelbel.movies.interactor.usecase.SelectFeedViewCase
-import org.michaelbel.movies.interactor.usecase.SelectLanguageCase
-import org.michaelbel.movies.interactor.usecase.SelectMovieListCase
-import org.michaelbel.movies.interactor.usecase.SelectThemeCase
-import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val interactor: Interactor,
- private val selectLanguageCase: SelectLanguageCase,
- private val selectThemeCase: SelectThemeCase,
- private val selectFeedViewCase: SelectFeedViewCase,
- private val selectMovieListCase: SelectMovieListCase,
+ private val localeController: LocaleController,
private val delayUseCase: DelayUseCase
): BaseViewModel(), DefaultLifecycleObserver {
@@ -98,23 +92,23 @@ class SettingsViewModel @Inject constructor(
.stateIn(
scope = this,
started = SharingStarted.Lazily,
- initialValue = AppVersionData.None
+ initialValue = AppVersionData.Empty
)
fun selectLanguage(language: AppLanguage) = launch {
- selectLanguageCase(language)
+ localeController.selectLanguage(language)
}
fun selectTheme(theme: AppTheme) = launch {
- selectThemeCase(theme)
+ interactor.selectTheme(theme)
}
fun selectFeedView(feedView: FeedView) = launch {
- selectFeedViewCase(feedView)
+ interactor.selectFeedView(feedView)
}
fun selectMovieList(movieList: MovieList) = launch {
- selectMovieListCase(movieList)
+ interactor.selectMovieList(movieList)
}
fun setDynamicColors(value: Boolean) = launch {
diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/IconAliasKtx.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/IconAliasKtx.kt
new file mode 100644
index 000000000..dd99bad1a
--- /dev/null
+++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/IconAliasKtx.kt
@@ -0,0 +1,33 @@
+package org.michaelbel.movies.settings.ktx
+
+import android.content.Context
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import org.michaelbel.movies.settings_impl.R
+import org.michaelbel.movies.ui.appicon.IconAlias
+import org.michaelbel.movies.ui.appicon.isEnabled
+
+internal val IconAlias.iconText: String
+ @Composable get() = when (this) {
+ is IconAlias.Red -> stringResource(R.string.settings_app_launcher_icon_red)
+ is IconAlias.Purple -> stringResource(R.string.settings_app_launcher_icon_purple)
+ is IconAlias.Brown -> stringResource(R.string.settings_app_launcher_icon_brown)
+ }
+
+internal fun IconAlias.iconSnackbarText(context: Context): String {
+ return when (this) {
+ is IconAlias.Red -> context.getString(R.string.settings_app_launcher_icon_red)
+ is IconAlias.Purple -> context.getString(R.string.settings_app_launcher_icon_purple)
+ is IconAlias.Brown -> context.getString(R.string.settings_app_launcher_icon_brown)
+ }
+}
+
+@Composable
+internal fun IconAlias.backgroundColor(context: Context): Color {
+ return when {
+ context.isEnabled(this) -> MaterialTheme.colorScheme.inversePrimary
+ else -> MaterialTheme.colorScheme.primaryContainer
+ }
+}
\ No newline at end of file
diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/AppIconBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/AppIconBox.kt
new file mode 100644
index 000000000..25363b4bf
--- /dev/null
+++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/AppIconBox.kt
@@ -0,0 +1,101 @@
+package org.michaelbel.movies.settings.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ChainStyle
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.Dimension
+import org.michaelbel.movies.settings.ktx.backgroundColor
+import org.michaelbel.movies.settings.ktx.iconText
+import org.michaelbel.movies.ui.appicon.IconAlias
+import org.michaelbel.movies.ui.appicon.isEnabled
+import org.michaelbel.movies.ui.ktx.context
+import org.michaelbel.movies.ui.preview.DevicePreviews
+import org.michaelbel.movies.ui.preview.provider.IconAliasPreviewParameterProvider
+import org.michaelbel.movies.ui.theme.MoviesTheme
+
+@Composable
+fun AppIconBox(
+ iconAlias: IconAlias,
+ modifier: Modifier = Modifier
+) {
+ ConstraintLayout(
+ modifier = modifier
+ ) {
+ val (icon, radio, text) = createRefs()
+ createHorizontalChain(radio, text, chainStyle = ChainStyle.Packed)
+
+ Icon(
+ painter = painterResource(iconAlias.iconRes),
+ contentDescription = null,
+ modifier = Modifier
+ .constrainAs(icon) {
+ width = Dimension.value(56.dp)
+ height = Dimension.value(56.dp)
+ start.linkTo(parent.start, 8.dp)
+ top.linkTo(parent.top, 8.dp)
+ end.linkTo(parent.end, 8.dp)
+ }
+ .clip(CircleShape),
+ tint = Color.Unspecified
+ )
+
+ RadioButton(
+ selected = context.isEnabled(iconAlias),
+ onClick = null,
+ modifier = Modifier.constrainAs(radio) {
+ width = Dimension.wrapContent
+ height = Dimension.wrapContent
+ start.linkTo(parent.start)
+ top.linkTo(icon.bottom, 8.dp)
+ end.linkTo(text.start)
+ bottom.linkTo(parent.bottom, 8.dp)
+ }
+ )
+
+ Text(
+ text = iconAlias.iconText,
+ modifier = Modifier
+ .constrainAs(text) {
+ width = Dimension.wrapContent
+ height = Dimension.wrapContent
+ start.linkTo(radio.end)
+ top.linkTo(radio.top)
+ end.linkTo(parent.end)
+ bottom.linkTo(radio.bottom)
+ }
+ .padding(start = 2.dp),
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ )
+ }
+}
+
+@Composable
+@DevicePreviews
+private fun AppIconBoxPreview(
+ @PreviewParameter(IconAliasPreviewParameterProvider::class) iconAlias: IconAlias
+) {
+ MoviesTheme {
+ AppIconBox(
+ iconAlias = iconAlias,
+ modifier = Modifier
+ .clip(RoundedCornerShape(8.dp))
+ .background(iconAlias.backgroundColor(context))
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppIconBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppIconBox.kt
new file mode 100644
index 000000000..eeb685577
--- /dev/null
+++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppIconBox.kt
@@ -0,0 +1,116 @@
+package org.michaelbel.movies.settings.ui
+
+import android.content.Context
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.Dimension
+import org.michaelbel.movies.settings.ktx.backgroundColor
+import org.michaelbel.movies.settings_impl.R
+import org.michaelbel.movies.ui.appicon.IconAlias
+import org.michaelbel.movies.ui.appicon.setIcon
+import org.michaelbel.movies.ui.preview.DevicePreviews
+import org.michaelbel.movies.ui.theme.MoviesTheme
+
+@Composable
+fun SettingsAppIconBox(
+ onAppIconChanged: (IconAlias) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val context: Context = LocalContext.current
+
+ fun changeAppIcon(iconAlias: IconAlias) {
+ onAppIconChanged(iconAlias)
+ context.setIcon(iconAlias)
+ }
+
+ ConstraintLayout(
+ modifier = modifier
+ ) {
+ val (title, redBox, purpleBox, brownBox) = createRefs()
+
+ Text(
+ text = stringResource(R.string.settings_app_launcher_icon),
+ modifier = Modifier.constrainAs(title) {
+ width = Dimension.wrapContent
+ height = Dimension.wrapContent
+ start.linkTo(parent.start, 16.dp)
+ top.linkTo(parent.top, 8.dp)
+ },
+ style = MaterialTheme.typography.bodyLarge.copy(
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ )
+
+ AppIconBox(
+ iconAlias = IconAlias.Red,
+ modifier = Modifier
+ .constrainAs(redBox) {
+ height = Dimension.wrapContent
+ start.linkTo(parent.start, 8.dp)
+ top.linkTo(title.bottom, 8.dp)
+ end.linkTo(purpleBox.start, 4.dp)
+ bottom.linkTo(parent.bottom, 16.dp)
+ }
+ .fillMaxWidth(.3F)
+ .clip(RoundedCornerShape(8.dp))
+ .background(IconAlias.Red.backgroundColor(context))
+ .clickable { changeAppIcon(IconAlias.Red) }
+ )
+
+ AppIconBox(
+ iconAlias = IconAlias.Purple,
+ modifier = Modifier
+ .constrainAs(purpleBox) {
+ height = Dimension.wrapContent
+ start.linkTo(redBox.end, 4.dp)
+ top.linkTo(title.bottom, 8.dp)
+ end.linkTo(brownBox.start, 4.dp)
+ bottom.linkTo(parent.bottom, 16.dp)
+ }
+ .fillMaxWidth(.3F)
+ .clip(RoundedCornerShape(8.dp))
+ .background(IconAlias.Purple.backgroundColor(context))
+ .clickable { changeAppIcon(IconAlias.Purple) }
+ )
+
+ AppIconBox(
+ iconAlias = IconAlias.Brown,
+ modifier = Modifier
+ .constrainAs(brownBox) {
+ height = Dimension.wrapContent
+ start.linkTo(purpleBox.end, 4.dp)
+ top.linkTo(title.bottom, 8.dp)
+ end.linkTo(parent.end, 8.dp)
+ bottom.linkTo(parent.bottom, 16.dp)
+ }
+ .fillMaxWidth(.3F)
+ .clip(RoundedCornerShape(8.dp))
+ .background(IconAlias.Brown.backgroundColor(context))
+ .clickable { changeAppIcon(IconAlias.Brown) }
+ )
+ }
+}
+
+@Composable
+@DevicePreviews
+private fun SettingsAppIconBoxPreview() {
+ MoviesTheme {
+ SettingsAppIconBox(
+ onAppIconChanged = {},
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceDialog.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceDialog.kt
index a70ebdcbb..8ba4ba422 100644
--- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceDialog.kt
+++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsAppearanceDialog.kt
@@ -1,12 +1,15 @@
package org.michaelbel.movies.settings.ui
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
@@ -21,7 +24,6 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.DialogProperties
import org.michaelbel.movies.common.appearance.FeedView
import org.michaelbel.movies.settings.ktx.feedViewText
import org.michaelbel.movies.settings_impl.R
@@ -81,11 +83,7 @@ fun SettingsAppearanceDialog(
shape = RoundedCornerShape(28.dp),
containerColor = MaterialTheme.colorScheme.surface,
iconContentColor = MaterialTheme.colorScheme.secondary,
- titleContentColor = MaterialTheme.colorScheme.onSurface,
- properties = DialogProperties(
- dismissOnBackPress = true,
- dismissOnClickOutside = false
- )
+ titleContentColor = MaterialTheme.colorScheme.onSurface
)
}
@@ -95,17 +93,17 @@ private fun SettingAppearanceDialogContent(
onFeedViewSelect: (FeedView) -> Unit,
modifier: Modifier = Modifier
) {
+ val scrollState: ScrollState = rememberScrollState()
+
Column(
- modifier = modifier
+ modifier = modifier.verticalScroll(scrollState)
) {
FeedView.VALUES.forEach { feedView: FeedView ->
Row(
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
- .clickable {
- onFeedViewSelect(feedView)
- },
+ .clickable { onFeedViewSelect(feedView) },
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageDialog.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageDialog.kt
index 23753e7ea..679f4335d 100644
--- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageDialog.kt
+++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageDialog.kt
@@ -1,12 +1,15 @@
package org.michaelbel.movies.settings.ui
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
@@ -21,7 +24,6 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.DialogProperties
import org.michaelbel.movies.common.localization.model.AppLanguage
import org.michaelbel.movies.settings.ktx.languageText
import org.michaelbel.movies.settings_impl.R
@@ -81,11 +83,7 @@ fun SettingLanguageDialog(
shape = RoundedCornerShape(28.dp),
containerColor = MaterialTheme.colorScheme.surface,
iconContentColor = MaterialTheme.colorScheme.secondary,
- titleContentColor = MaterialTheme.colorScheme.onSurface,
- properties = DialogProperties(
- dismissOnBackPress = true,
- dismissOnClickOutside = false
- )
+ titleContentColor = MaterialTheme.colorScheme.onSurface
)
}
@@ -95,17 +93,17 @@ private fun SettingLanguageDialogContent(
onLanguageSelect: (AppLanguage) -> Unit,
modifier: Modifier = Modifier
) {
+ val scrollState: ScrollState = rememberScrollState()
+
Column(
- modifier = modifier
+ modifier = modifier.verticalScroll(scrollState)
) {
AppLanguage.VALUES.forEach { language: AppLanguage ->
Row(
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
- .clickable {
- onLanguageSelect(language)
- },
+ .clickable { onLanguageSelect(language) },
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListDialog.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListDialog.kt
index 1038968c2..1a842d988 100644
--- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListDialog.kt
+++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsMovieListDialog.kt
@@ -1,12 +1,15 @@
package org.michaelbel.movies.settings.ui
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
@@ -21,7 +24,6 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.DialogProperties
import org.michaelbel.movies.common.list.MovieList
import org.michaelbel.movies.settings.ktx.listText
import org.michaelbel.movies.settings_impl.R
@@ -81,11 +83,7 @@ fun SettingsMovieListDialog(
shape = RoundedCornerShape(28.dp),
containerColor = MaterialTheme.colorScheme.surface,
iconContentColor = MaterialTheme.colorScheme.secondary,
- titleContentColor = MaterialTheme.colorScheme.onSurface,
- properties = DialogProperties(
- dismissOnBackPress = true,
- dismissOnClickOutside = false
- )
+ titleContentColor = MaterialTheme.colorScheme.onSurface
)
}
@@ -101,18 +99,17 @@ private fun SettingMovieListDialogContent(
MovieList.TopRated,
MovieList.Upcoming
)
+ val scrollState: ScrollState = rememberScrollState()
Column(
- modifier = modifier
+ modifier = modifier.verticalScroll(scrollState)
) {
movieLists.forEach { movieList ->
Row(
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
- .clickable {
- onMovieListSelect(movieList)
- },
+ .clickable { onMovieListSelect(movieList) },
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsPostNotificationsBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsPostNotificationsBox.kt
index 2436ad3a0..b41515ccf 100644
--- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsPostNotificationsBox.kt
+++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsPostNotificationsBox.kt
@@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import org.michaelbel.movies.common.ktx.notificationManager
-import org.michaelbel.movies.notifications.ktx.appNotificationSettingsIntent
+import org.michaelbel.movies.ui.ktx.appNotificationSettingsIntent
import org.michaelbel.movies.settings_impl.R
import org.michaelbel.movies.ui.lifecycle.OnResume
import org.michaelbel.movies.ui.preview.DevicePreviews
diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsScreenContent.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsScreenContent.kt
index 188672c28..3a783837a 100644
--- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsScreenContent.kt
+++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsScreenContent.kt
@@ -4,24 +4,29 @@ import android.app.Activity
import android.content.Context
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
@@ -39,10 +44,13 @@ import org.michaelbel.movies.common.review.rememberReviewManager
import org.michaelbel.movies.common.review.rememberReviewTask
import org.michaelbel.movies.common.theme.AppTheme
import org.michaelbel.movies.common.version.AppVersionData
-import org.michaelbel.movies.notifications.ktx.appNotificationSettingsIntent
import org.michaelbel.movies.settings.SettingsViewModel
+import org.michaelbel.movies.settings.ktx.iconSnackbarText
import org.michaelbel.movies.settings_impl.BuildConfig
import org.michaelbel.movies.settings_impl.R
+import org.michaelbel.movies.ui.ktx.appNotificationSettingsIntent
+import org.michaelbel.movies.ui.ktx.clickableWithoutRipple
+import org.michaelbel.movies.ui.ktx.displayCutoutWindowInsets
import org.michaelbel.movies.ui.R as UiR
@Composable
@@ -118,6 +126,8 @@ private fun SettingsScreenContent(
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
val reviewManager: ReviewManager = rememberReviewManager()
val reviewInfo: ReviewInfo? = rememberReviewTask(reviewManager)
+ val topAppBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+ val lazyListState: LazyListState = rememberLazyListState()
val resultContract = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
@@ -145,6 +155,12 @@ private fun SettingsScreenContent(
}
}
+ val onScrollToTop: () -> Unit = {
+ scope.launch {
+ lazyListState.animateScrollToItem(0)
+ }
+ }
+
fun onLaunchReviewFlow() {
when {
!isPlayServicesAvailable -> {
@@ -162,10 +178,14 @@ private fun SettingsScreenContent(
}
Scaffold(
- modifier = modifier,
+ modifier = modifier
+ .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
topBar = {
SettingsToolbar(
- modifier = Modifier.statusBarsPadding(),
+ topAppBarScrollBehavior = topAppBarScrollBehavior,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickableWithoutRipple { onScrollToTop() },
onNavigationIconClick = onBackClick
)
},
@@ -174,7 +194,9 @@ private fun SettingsScreenContent(
appVersionData = appVersionData,
modifier = Modifier
.navigationBarsPadding()
+ .windowInsetsPadding(displayCutoutWindowInsets)
.fillMaxWidth()
+ .background(MaterialTheme.colorScheme.primaryContainer)
)
},
snackbarHost = {
@@ -184,87 +206,109 @@ private fun SettingsScreenContent(
},
containerColor = MaterialTheme.colorScheme.primaryContainer
) { paddingValues ->
- Column(
- modifier = Modifier.padding(paddingValues)
+ LazyColumn(
+ modifier = Modifier
+ .navigationBarsPadding()
+ .windowInsetsPadding(displayCutoutWindowInsets),
+ state = lazyListState,
+ contentPadding = paddingValues
) {
- SettingsLanguageBox(
- currentLanguage = currentLanguage,
- onLanguageSelect = onLanguageSelect,
- modifier = Modifier
- .fillMaxWidth()
- .height(52.dp)
- )
-
- SettingsThemeBox(
- currentTheme = currentTheme,
- onThemeSelect = onThemeSelect,
- modifier = Modifier
- .fillMaxWidth()
- .height(52.dp)
- )
-
- SettingsAppearanceBox(
- currentFeedView = currentFeedView,
- onFeedViewSelect = onFeedViewSelect,
- modifier = Modifier
- .fillMaxWidth()
- .height(52.dp)
- )
-
- SettingsMovieListBox(
- currentMovieList = currentMovieList,
- onMovieListSelect = onMovieListSelect,
- modifier = Modifier
- .fillMaxWidth()
- .height(52.dp)
- )
-
- if (isDynamicColorsFeatureEnabled) {
- SettingsDynamicColorsBox(
- isDynamicColorsEnabled = dynamicColors,
+ item {
+ SettingsLanguageBox(
+ currentLanguage = currentLanguage,
+ onLanguageSelect = onLanguageSelect,
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
- .clickable {
- onSetDynamicColors(!dynamicColors)
- }
)
}
-
- if (isRtlFeatureEnabled) {
- SettingsRtlBox(
- isRtlEnabled = isRtlEnabled,
+ item {
+ SettingsThemeBox(
+ currentTheme = currentTheme,
+ onThemeSelect = onThemeSelect,
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
- .clickable {
- onEnableRtlChanged(!isRtlEnabled)
- }
)
}
-
- if (isPostNotificationsFeatureEnabled) {
- SettingsPostNotificationsBox(
+ item {
+ SettingsAppearanceBox(
+ currentFeedView = currentFeedView,
+ onFeedViewSelect = onFeedViewSelect,
modifier = Modifier
.fillMaxWidth()
- .height(52.dp),
- onShowPermissionSnackbar = onShowPermissionSnackbar
+ .height(52.dp)
)
}
-
- SettingsReviewBox(
- modifier = Modifier
- .fillMaxWidth()
- .height(52.dp)
- .clickable {
- onLaunchReviewFlow()
- }
- )
-
- if (BuildConfig.DEBUG) {
- SettingsNetworkRequestDelayBox(
- delay = networkRequestDelay,
- onDelayChangeFinished = onDelayChangeFinished,
+ item {
+ SettingsMovieListBox(
+ currentMovieList = currentMovieList,
+ onMovieListSelect = onMovieListSelect,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp)
+ )
+ }
+ item {
+ if (isDynamicColorsFeatureEnabled) {
+ SettingsDynamicColorsBox(
+ isDynamicColorsEnabled = dynamicColors,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp)
+ .clickable {
+ onSetDynamicColors(!dynamicColors)
+ }
+ )
+ }
+ }
+ item {
+ if (isRtlFeatureEnabled) {
+ SettingsRtlBox(
+ isRtlEnabled = isRtlEnabled,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp)
+ .clickable {
+ onEnableRtlChanged(!isRtlEnabled)
+ }
+ )
+ }
+ }
+ item {
+ if (isPostNotificationsFeatureEnabled) {
+ SettingsPostNotificationsBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp),
+ onShowPermissionSnackbar = onShowPermissionSnackbar
+ )
+ }
+ }
+ item {
+ SettingsReviewBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp)
+ .clickable {
+ onLaunchReviewFlow()
+ }
+ )
+ }
+ item {
+ if (BuildConfig.DEBUG) {
+ SettingsNetworkRequestDelayBox(
+ delay = networkRequestDelay,
+ onDelayChangeFinished = onDelayChangeFinished,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ item {
+ SettingsAppIconBox(
+ onAppIconChanged = { iconAlias ->
+ onShowSnackbar(context.getString(R.string.settings_app_launcher_icon_changed_to, iconAlias.iconSnackbarText(context)))
+ },
modifier = Modifier.fillMaxWidth()
)
}
diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeDialog.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeDialog.kt
index 66137cf79..cefbea4bb 100644
--- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeDialog.kt
+++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeDialog.kt
@@ -1,12 +1,15 @@
package org.michaelbel.movies.settings.ui
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
@@ -22,7 +25,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.DialogProperties
import org.michaelbel.movies.common.theme.AppTheme
import org.michaelbel.movies.settings.ktx.themeText
import org.michaelbel.movies.settings_impl.R
@@ -82,11 +84,7 @@ internal fun SettingThemeDialog(
shape = RoundedCornerShape(28.dp),
containerColor = MaterialTheme.colorScheme.surface,
iconContentColor = MaterialTheme.colorScheme.secondary,
- titleContentColor = MaterialTheme.colorScheme.onSurface,
- properties = DialogProperties(
- dismissOnBackPress = true,
- dismissOnClickOutside = false
- )
+ titleContentColor = MaterialTheme.colorScheme.onSurface
)
}
@@ -96,17 +94,17 @@ private fun SettingThemeDialogContent(
onThemeSelect: (AppTheme) -> Unit,
modifier: Modifier = Modifier
) {
+ val scrollState: ScrollState = rememberScrollState()
+
Column(
- modifier = modifier
+ modifier = modifier.verticalScroll(scrollState)
) {
AppTheme.VALUES.forEach { theme: AppTheme ->
Row(
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
- .clickable {
- onThemeSelect(theme)
- },
+ .clickable { onThemeSelect(theme) },
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsToolbar.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsToolbar.kt
index 8bd9ebd10..9d9e7d98d 100644
--- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsToolbar.kt
+++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsToolbar.kt
@@ -2,27 +2,30 @@ package org.michaelbel.movies.settings.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import org.michaelbel.movies.settings_impl.R
import org.michaelbel.movies.ui.icons.MoviesIcons
+import org.michaelbel.movies.ui.ktx.displayCutoutWindowInsets
import org.michaelbel.movies.ui.preview.DevicePreviews
import org.michaelbel.movies.ui.theme.MoviesTheme
@Composable
internal fun SettingsToolbar(
+ topAppBarScrollBehavior: TopAppBarScrollBehavior,
modifier: Modifier = Modifier,
- onNavigationIconClick: () -> Unit
+ onNavigationIconClick: () -> Unit,
) {
TopAppBar(
title = {
@@ -40,7 +43,9 @@ internal fun SettingsToolbar(
navigationIcon = {
IconButton(
onClick = onNavigationIconClick,
- modifier = Modifier.testTag("BackIconButton")
+ modifier = Modifier
+ .windowInsetsPadding(displayCutoutWindowInsets)
+ .testTag("BackIconButton")
) {
Image(
imageVector = MoviesIcons.ArrowBack,
@@ -51,8 +56,10 @@ internal fun SettingsToolbar(
}
},
colors = TopAppBarDefaults.topAppBarColors(
- containerColor = Color.Transparent
- )
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ scrolledContainerColor = MaterialTheme.colorScheme.inversePrimary
+ ),
+ scrollBehavior = topAppBarScrollBehavior
)
}
@@ -62,7 +69,8 @@ private fun SettingsToolbarPreview() {
MoviesTheme {
SettingsToolbar(
modifier = Modifier.statusBarsPadding(),
- onNavigationIconClick = {}
+ onNavigationIconClick = {},
+ topAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
)
}
}
\ No newline at end of file
diff --git a/feature/settings-impl/src/main/res/values-ru/strings.xml b/feature/settings-impl/src/main/res/values-ru/strings.xml
index 23d916b67..c36a90fbd 100644
--- a/feature/settings-impl/src/main/res/values-ru/strings.xml
+++ b/feature/settings-impl/src/main/res/values-ru/strings.xml
@@ -25,6 +25,11 @@
Скоро
Задержка сетевого запроса
%s мс
+ Иконка Приложения
+ Красная
+ Фиолетовая
+ Коричневая
+ Иконка приложения изменена на %s
Movies v%s
(%s)
Debug
diff --git a/feature/settings-impl/src/main/res/values/strings.xml b/feature/settings-impl/src/main/res/values/strings.xml
index bbb7e180d..d94563bcd 100644
--- a/feature/settings-impl/src/main/res/values/strings.xml
+++ b/feature/settings-impl/src/main/res/values/strings.xml
@@ -25,6 +25,11 @@
Upcoming
Network Request Delay
%s ms
+ App Icon
+ Red
+ Purple
+ Brown
+ App icon changed to %s
Movies v%s
(%s)
Debug
diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts
index 1f2a4bac4..c11ef7cbf 100644
--- a/feature/settings/build.gradle.kts
+++ b/feature/settings/build.gradle.kts
@@ -2,7 +2,6 @@
plugins {
alias(libs.plugins.library)
alias(libs.plugins.kotlin)
- alias(libs.plugins.detekt)
}
android {
diff --git a/gradle.properties b/gradle.properties
index afd38abde..c44943ed7 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,16 +20,4 @@ android.defaults.buildFeatures.resValues=false
# https://d.android.com/reference/tools/gradle-api/8.0/com/android/build/api/dsl/BuildFeatures#shaders()
# Flag to enable Shader compilation.
# Default value is true.
-android.defaults.buildFeatures.shaders=false
-
-
-
-# Use latest lint alpha for best available K2 support
-# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html
-android.experimental.lint.version=8.2.0-alpha15
-
-# Use K2 compiler
-kotlin.experimental.tryK2=true
-
-# Run lint on K2
-android.lint.useK2Uast=true
\ No newline at end of file
+android.defaults.buildFeatures.shaders=false
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index eec81feb8..9e672e24d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,15 +1,13 @@
-# Compose to Kotlin Compatibility Map:
-# https://d.android.com/jetpack/androidx/releases/compose-kotlin
+# Compose to Kotlin Compatibility Map: https://d.android.com/jetpack/androidx/releases/compose-kotlin
[versions]
-min-sdk = "21"
-benchmark-min-sdk = "23"
+min-sdk = "23"
compile-sdk = "34"
target-sdk = "34"
-gradle = "8.1.2"
-kotlin = "1.9.10"
-kotlin-ksp = "1.9.10-1.0.13"
+gradle = "8.1.4"
+kotlin = "1.9.20"
+kotlin-ksp = "1.9.20-1.0.14"
kotlin-coroutines = "1.7.3"
-kotlin-serialization-json = "1.6.0"
+kotlin-serialization-json = "1.6.1"
detekt = "1.23.3"
spotless = "6.22.0"
google-services = "4.4.0"
@@ -30,13 +28,13 @@ hilt = "2.48.1"
androidx-compose-foundation = "1.5.4"
androidx-compose-runtime = "1.5.4"
androidx-compose-ui = "1.5.4"
-androidx-compose-compiler = "1.5.3"
+androidx-compose-compiler = "1.5.4"
androidx-compose-material = "1.5.4"
androidx-compose-material3 = "1.1.2"
androidx-appcompat = "1.6.1"
-androidx-activity = "1.8.0"
+androidx-activity = "1.8.1"
androidx-autofill = "1.1.0"
-androidx-browser = "1.6.0"
+androidx-browser = "1.7.0"
androidx-core-ktx = "1.12.0"
androidx-core-splashscreen = "1.0.1"
androidx-constraintlayout = "1.0.1"
@@ -52,9 +50,9 @@ androidx-test = "1.5.2"
androidx-test-ext = "1.1.5"
androidx-test-uiautomator = "2.2.0"
androidx-espresso-core = "3.5.1"
-androidx-benchmark = "1.2.0"
+androidx-benchmark = "1.2.1"
androidx-profile-installer = "1.3.1"
-androidx-work = "2.8.1"
+androidx-work = "2.9.0-rc01"
coil = "2.5.0"
okhttp = "4.12.0"
retrofit = "2.9.0"
diff --git a/instant/build.gradle.kts b/instant/build.gradle.kts
index 39d9565f3..6ab87a971 100644
--- a/instant/build.gradle.kts
+++ b/instant/build.gradle.kts
@@ -27,6 +27,7 @@ android {
dependencies {
implementation(project(":app"))
+ implementation(project(":core:common"))
implementation(project(":core:ui"))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
diff --git a/instant/src/main/AndroidManifest.xml b/instant/src/main/AndroidManifest.xml
index 0ae7b05a9..a9a54bb91 100644
--- a/instant/src/main/AndroidManifest.xml
+++ b/instant/src/main/AndroidManifest.xml
@@ -25,10 +25,10 @@
android:exported="true"
android:theme="@style/Theme.Movies">
-
+
diff --git a/readme.md b/readme.md
index 1d829be29..a96bce92a 100644
--- a/readme.md
+++ b/readme.md
@@ -17,6 +17,8 @@ Movies - easy way to discover popular movies. This is a simple TMDb client for A
+
+
## Build
@@ -29,7 +31,7 @@ TMDB_API_KEY=your_own_tmdb_api_key
## Download
[](https://play.google.com/store/apps/details?id=org.michaelbel.moviemade)
-[](https://github.com/michaelbel/movies/releases/download/1.4.6/Movies-v1.4.6.1196.-release.apk)
+[](https://github.com/michaelbel/movies/releases/download/1.5.1/Movies-v1.5.1.1347.-release.apk)
## Technologies
@@ -41,15 +43,15 @@ TMDB_API_KEY=your_own_tmdb_api_key
- [x] [Kotlin Symbol Processing API](https://d.android.com/studio/build/migrate-to-ksp)
- [x] [Gradle Plugin](https://d.android.com/studio/releases/gradle-plugin)
- [x] [Gradle Version Catalog](https://d.android.com/build/migrate-to-catalogs)
-- [x] MinSDK 21
+- [x] MinSDK 23
- [x] TargetSDK 34
- [x] CompileSDK 34
- [x] [Material3](https://m3.material.io)
- [x] [Dark Theme](https://d.android.com/develop/ui/views/theming/darktheme)
-- [x] [Dynamic Colors](https://d.android.com/develop/ui/views/theming/dynamic-colors)
+- [x] [Material You Dynamic Colors](https://d.android.com/develop/ui/views/theming/dynamic-colors)
- [x] [Themed App Icon](https://d.android.com/develop/ui/views/launch/icon_design_adaptive)
-- [x] 100% [Kotlin](https://d.android.com/kotlin)
-- [x] 100% [Jetpack Compose](https://d.android.com/jetpack/compose)
+- [x] [Kotlin](https://d.android.com/kotlin)
+- [x] [Jetpack Compose](https://d.android.com/jetpack/compose)
- [x] [Accompanist](https://github.com/google/accompanist)
- [x] [Compose PreviewParameterProvider](https://d.android.com/jetpack/compose/tooling#previewparameter)
- [x] [Downloadable Fonts](https://d.android.com/develop/ui/views/text-and-emoji/downloadable-fonts)
@@ -64,7 +66,7 @@ TMDB_API_KEY=your_own_tmdb_api_key
- [x] [DataStore](https://d.android.com/datastore)
- [x] [Startup](https://d.android.com/jetpack/androidx/releases/startup)
- [x] [Navigation](https://d.android.com/guide/navigation)
-- [x] [Paging](https://d.android.com/topic/libraries/architecture/paging/v3-overview) (RemoteMediator & PagingSource)
+- [x] [Paging3](https://d.android.com/topic/libraries/architecture/paging/v3-overview)
- [x] [ConstraintLayout](https://d.android.com/develop/ui/views/layout/constraint-layout)
- [x] [Browser](https://d.android.com/jetpack/androidx/releases/browser)
- [x] [OkHttp](https://github.com/square/okhttp)
@@ -82,7 +84,7 @@ TMDB_API_KEY=your_own_tmdb_api_key
- [x] [In-App Updates](https://d.android.com/guide/playcore/in-app-updates)
- [x] [App Shortcuts](https://d.android.com/develop/ui/views/launch/shortcuts)
- [x] [Dependabot](https://github.com/dependabot)
-- [x] [Github Actions](https://github.com/michaelbel/movies/tree/develop/.github/workflows) CI/CD
+- [x] [Github Actions](https://github.com/michaelbel/movies/tree/develop/.github/workflows)
- [x] [Github Releases](https://github.com/michaelbel/movies/releases)
- [x] [Lint](https://d.android.com/studio/write/lint)
- [x] [Detekt](https://github.com/detekt/detekt)
@@ -95,16 +97,23 @@ TMDB_API_KEY=your_own_tmdb_api_key
- [x] [Benchmark](https://d.android.com/topic/performance/benchmarking/benchmarking-overview)
- [x] [Support Localization](https://d.android.com/guide/topics/resources/localization)
- [x] [Notification Runtime Permission](https://d.android.com/develop/ui/views/notifications/notification-permission)
+- [x] [Changing Launcher App Icon](https://d.android.com/guide/topics/manifest/activity-alias-element)
+- [x] [Predictive Back Gesture](https://d.android.com/guide/navigation/custom-back/predictive-back-gesture)
+- [x] [Codebeat Automated Code Review](https://codebeat.co/projects/github-com-michaelbel-movies-develop)
+- [x] [Codacy Static Code Analysis](https://app.codacy.com/gh/michaelbel/movies/dashboard)
+- [x] [Display Content Edge-to-Edge](https://d.android.com/develop/ui/views/layout/edge-to-edge)
+- [x] [Support Landscape Orientation](https://d.android.com/guide/topics/large-screens/support-different-screen-sizes)
+- [x] [Support Display Cutouts](https://d.android.com/jetpack/compose/system/cutouts)
- [ ] [Unit Tests](https://d.android.com/training/testing/local-tests)
- [ ] [UI Tests](https://d.android.com/training/testing/instrumented-tests/ui-tests)
- [ ] [Baseline Profiles](https://d.android.com/topic/performance/baselineprofiles/overview)
- [ ] [Tablet and large screen support](https://d.android.com/about/versions/13/features/large-screens)
- [ ] OAuth
- [ ] [Animations](https://d.android.com/develop/ui/views/animations)
-- [ ] Landscape Orientation
- [ ] [Support layout mirroring](https://d.android.com/training/basics/supporting-devices/languages#SupportLayoutMirroring)
- [ ] Upload Bundle to Google Play Console
- [ ] [ExoPlayer](https://d.android.com/guide/topics/media/exoplayer)
+- [ ] [Google Play Instant](https://d.android.com/topic/google-play-instant/overview)
## Issues
If you find any problems or would like to suggest a feature, please feel free to file an [issue](https://github.com/michaelbel/moviemade/issues).
diff --git a/settings.gradle.kts b/settings.gradle.kts
index c6dd8945f..3336a6870 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -23,8 +23,6 @@ include(
":core:analytics",
":core:common",
- ":core:domain",
- ":core:entities",
":core:interactor",
":core:interactor-impl",
":core:navigation",
@@ -34,6 +32,7 @@ include(
":core:repository",
":core:repository-impl",
":core:ui",
+ ":core:work",
":feature:account",
":feature:account-impl",