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",