diff --git a/.gitignore b/.gitignore index db94edc58..daeda5d1d 100644 --- a/.gitignore +++ b/.gitignore @@ -707,3 +707,4 @@ /buildSrc/.gradle/7.6/dependencies-accessors/eaf6e422b407037b3630f94e47d7baced32e5a49/sources/org/gradle/accessors/dm/RootProjectAccessor.java /.gradle/7.6/checksums/sha1-checksums.bin /buildSrc/.gradle/7.6/checksums/sha1-checksums.bin +/config/movies.jks diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c53f5cea3..fd4a34efc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,8 +1,7 @@ - import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties +import java.io.FileInputStream import org.apache.commons.io.output.ByteArrayOutputStream import org.jetbrains.kotlin.konan.properties.Properties -import java.io.FileInputStream plugins { id("movies-android-application") @@ -143,7 +142,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.kotlin.compiler.extension.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } lint { diff --git a/app/src/main/kotlin/org/michaelbel/movies/App.kt b/app/src/main/kotlin/org/michaelbel/movies/App.kt index 84471e0a4..11570871a 100644 --- a/app/src/main/kotlin/org/michaelbel/movies/App.kt +++ b/app/src/main/kotlin/org/michaelbel/movies/App.kt @@ -1,7 +1,22 @@ package org.michaelbel.movies import android.app.Application +import android.util.Log +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject +import org.michaelbel.moviemade.BuildConfig @HiltAndroidApp -internal class App: Application() \ No newline at end of file +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() + } +} \ 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 e9afd7526..dfc3988de 100644 --- a/app/src/main/kotlin/org/michaelbel/movies/MainActivity.kt +++ b/app/src/main/kotlin/org/michaelbel/movies/MainActivity.kt @@ -13,8 +13,12 @@ import androidx.core.view.WindowCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.workDataOf import dagger.hilt.android.AndroidEntryPoint import org.michaelbel.movies.common.shortcuts.installShortcuts +import org.michaelbel.movies.domain.workers.MoviesDatabaseWorker import org.michaelbel.movies.ui.theme.MoviesTheme import org.michaelbel.movies.ui.theme.model.AppTheme @@ -56,5 +60,18 @@ internal class MainActivity: AppCompatActivity() { } } } + + prepopulateDatabase() + } + + private fun prepopulateDatabase() { + val request = OneTimeWorkRequestBuilder() + .setInputData(workDataOf(MoviesDatabaseWorker.KEY_FILENAME to MOVIES_DATA_FILENAME)) + .build() + WorkManager.getInstance(this).enqueue(request) + } + + private companion object { + private const val MOVIES_DATA_FILENAME = "movies.json" } } \ 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 c68765f2d..ef628742e 100644 --- a/app/src/main/kotlin/org/michaelbel/movies/MainViewModel.kt +++ b/app/src/main/kotlin/org/michaelbel/movies/MainViewModel.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.michaelbel.movies.analytics.MoviesAnalytics -import org.michaelbel.movies.domain.interactor.SettingsInteractor +import org.michaelbel.movies.domain.interactor.settings.SettingsInteractor import org.michaelbel.movies.ui.theme.model.AppTheme @HiltViewModel diff --git a/build.gradle.kts b/build.gradle.kts index 9de5d5a97..e937bef3c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { alias(libs.plugins.library) apply false alias(libs.plugins.test) apply false alias(libs.plugins.kotlin) apply false + alias(libs.plugins.kotlin.ksp) apply false alias(libs.plugins.firebase.appdistribution) apply false alias(libs.plugins.firebase.crashlytics) apply false alias(libs.plugins.google.services) apply false @@ -12,9 +13,9 @@ plugins { alias(libs.plugins.spotless) alias(libs.plugins.detekt) - id("com.github.ben-manes.versions") version "0.45.0" + id("com.github.ben-manes.versions") version "0.46.0" id("nl.littlerobots.version-catalog-update") version "0.7.0" - id("com.palantir.git-version") version "0.15.0" + id("com.palantir.git-version") version "1.0.0" } /** diff --git a/config/screenshots/mockup2.png b/config/screenshots/mockup2.png deleted file mode 100644 index 55b13e241..000000000 Binary files a/config/screenshots/mockup2.png and /dev/null differ diff --git a/config/screenshots/screen1.png b/config/screenshots/screen1.png index ed732e617..47524c783 100644 Binary files a/config/screenshots/screen1.png and b/config/screenshots/screen1.png differ diff --git a/config/screenshots/screen2.png b/config/screenshots/screen2.png index a5c82de13..be285c27c 100644 Binary files a/config/screenshots/screen2.png and b/config/screenshots/screen2.png differ diff --git a/config/screenshots/screen3.png b/config/screenshots/screen3.png index 2b55021f9..da7b75c92 100644 Binary files a/config/screenshots/screen3.png and b/config/screenshots/screen3.png differ diff --git a/config/screenshots/screen4.png b/config/screenshots/screen4.png deleted file mode 100644 index f4ea8a26b..000000000 Binary files a/config/screenshots/screen4.png and /dev/null differ diff --git a/config/screenshots/screen5.png b/config/screenshots/screen5.png deleted file mode 100644 index fd07721c4..000000000 Binary files a/config/screenshots/screen5.png and /dev/null differ diff --git a/config/screenshots/screen6.png b/config/screenshots/screen6.png deleted file mode 100644 index 71b3dbd6e..000000000 Binary files a/config/screenshots/screen6.png and /dev/null differ diff --git a/core/analytics-impl/.gitignore b/core/analytics-impl/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/analytics-impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/analytics-impl/build.gradle.kts b/core/analytics-impl/build.gradle.kts new file mode 100644 index 000000000..b229f7fa6 --- /dev/null +++ b/core/analytics-impl/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("movies-android-library") + id("movies-android-hilt") +} + +android { + namespace = "org.michaelbel.movies.analytics_impl" + + defaultConfig { + minSdk = libs.versions.min.sdk.get().toInt() + compileSdk = libs.versions.compile.sdk.get().toInt() + } + + buildTypes { + create("benchmark") { + signingConfig = signingConfigs.getByName("debug") + matchingFallbacks += listOf("release") + initWith(getByName("release")) + } + } + + lint { + quiet = true + abortOnError = false + ignoreWarnings = true + checkDependencies = true + lintConfig = file("${project.rootDir}/config/codestyle/lint.xml") + } +} + +dependencies { + implementation(libs.firebase.analytics) +} \ No newline at end of file diff --git a/core/analytics-impl/src/main/AndroidManifest.xml b/core/analytics-impl/src/main/AndroidManifest.xml new file mode 100644 index 000000000..1d26c87a1 --- /dev/null +++ b/core/analytics-impl/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/model/MoviesEvents.kt b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesEvents.kt similarity index 88% rename from core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/model/MoviesEvents.kt rename to core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesEvents.kt index 7fdc92128..9894df34e 100644 --- a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/model/MoviesEvents.kt +++ b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesEvents.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.analytics.model +package org.michaelbel.movies.analytics.constants import com.google.firebase.analytics.FirebaseAnalytics diff --git a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/model/MoviesParams.kt b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesParams.kt similarity index 89% rename from core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/model/MoviesParams.kt rename to core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesParams.kt index 8b20296a3..d6a1377e3 100644 --- a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/model/MoviesParams.kt +++ b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesParams.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.analytics.model +package org.michaelbel.movies.analytics.constants import com.google.firebase.analytics.FirebaseAnalytics diff --git a/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/di/MoviesAnalyticsModule.kt b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/di/MoviesAnalyticsModule.kt new file mode 100644 index 000000000..89abdabbb --- /dev/null +++ b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/di/MoviesAnalyticsModule.kt @@ -0,0 +1,18 @@ +package org.michaelbel.movies.analytics.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import org.michaelbel.movies.analytics.MoviesAnalytics +import org.michaelbel.movies.analytics.impl.MoviesAnalyticsImpl + +@Module +@InstallIn(SingletonComponent::class) +internal interface MoviesAnalyticsModule { + + @Binds + @Singleton + fun provideMoviesAnalytics(analytics: MoviesAnalyticsImpl): MoviesAnalytics +} \ No newline at end of file diff --git a/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeDynamicColorsEvent.kt b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeDynamicColorsEvent.kt new file mode 100644 index 000000000..dcc67aaa1 --- /dev/null +++ b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeDynamicColorsEvent.kt @@ -0,0 +1,14 @@ +package org.michaelbel.movies.analytics.event + +import org.michaelbel.movies.analytics.constants.MoviesEvents +import org.michaelbel.movies.analytics.model.BaseEvent +import org.michaelbel.movies.analytics.constants.MoviesParams + +class ChangeDynamicColorsEvent( + enabled: Boolean +): BaseEvent(MoviesEvents.SETTINGS_CHANGE_DYNAMIC_COLORS) { + + init { + add(MoviesParams.PARAM_DYNAMIC_COLORS_ENABLED, enabled) + } +} \ No newline at end of file diff --git a/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeRtlEnabledEvent.kt b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeRtlEnabledEvent.kt new file mode 100644 index 000000000..486a70fd5 --- /dev/null +++ b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeRtlEnabledEvent.kt @@ -0,0 +1,14 @@ +package org.michaelbel.movies.analytics.event + +import org.michaelbel.movies.analytics.model.BaseEvent +import org.michaelbel.movies.analytics.constants.MoviesEvents +import org.michaelbel.movies.analytics.constants.MoviesParams + +class ChangeRtlEnabledEvent( + enabled: Boolean +): BaseEvent(MoviesEvents.SETTINGS_CHANGE_RTL_ENABLED) { + + init { + add(MoviesParams.PARAM_RTL_ENABLED, enabled) + } +} \ No newline at end of file diff --git a/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectLanguageEvent.kt b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectLanguageEvent.kt new file mode 100644 index 000000000..c7c343e6f --- /dev/null +++ b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectLanguageEvent.kt @@ -0,0 +1,14 @@ +package org.michaelbel.movies.analytics.event + +import org.michaelbel.movies.analytics.model.BaseEvent +import org.michaelbel.movies.analytics.constants.MoviesEvents +import org.michaelbel.movies.analytics.constants.MoviesParams + +class SelectLanguageEvent( + language: String +): BaseEvent(MoviesEvents.SETTINGS_SELECT_LANGUAGE) { + + init { + add(MoviesParams.PARAM_SELECTED_LANGUAGE, language) + } +} \ No newline at end of file diff --git a/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectThemeEvent.kt b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectThemeEvent.kt new file mode 100644 index 000000000..c0f1af152 --- /dev/null +++ b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectThemeEvent.kt @@ -0,0 +1,14 @@ +package org.michaelbel.movies.analytics.event + +import org.michaelbel.movies.analytics.constants.MoviesEvents +import org.michaelbel.movies.analytics.model.BaseEvent +import org.michaelbel.movies.analytics.constants.MoviesParams + +class SelectThemeEvent( + theme: String +): BaseEvent(MoviesEvents.SETTINGS_SELECT_THEME) { + + init { + add(MoviesParams.PARAM_SELECTED_THEME, theme) + } +} \ No newline at end of file diff --git a/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/model/BaseEvent.kt b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/model/BaseEvent.kt new file mode 100644 index 000000000..1d6a3a268 --- /dev/null +++ b/core/analytics-impl/src/main/kotlin/org/michaelbel/movies/analytics/model/BaseEvent.kt @@ -0,0 +1,14 @@ +package org.michaelbel.movies.analytics.model + +import android.os.Bundle +import androidx.core.os.bundleOf + +open class BaseEvent( + val name: String +) { + val params: Bundle = bundleOf() + + fun add(key: String, value: Any) { + params.putString(key, value.toString()) + } +} \ No newline at end of file diff --git a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesEvents.kt b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesEvents.kt new file mode 100644 index 000000000..9894df34e --- /dev/null +++ b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesEvents.kt @@ -0,0 +1,12 @@ +package org.michaelbel.movies.analytics.constants + +import com.google.firebase.analytics.FirebaseAnalytics + +internal object MoviesEvents { + const val SCREEN_VIEW = FirebaseAnalytics.Event.SCREEN_VIEW + + const val SETTINGS_SELECT_LANGUAGE = "select_language" + const val SETTINGS_SELECT_THEME = "select_theme" + const val SETTINGS_CHANGE_DYNAMIC_COLORS = "change_dynamic_colors" + const val SETTINGS_CHANGE_RTL_ENABLED = "change_rtl_enabled" +} \ No newline at end of file diff --git a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesParams.kt b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesParams.kt new file mode 100644 index 000000000..d6a1377e3 --- /dev/null +++ b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/constants/MoviesParams.kt @@ -0,0 +1,13 @@ +package org.michaelbel.movies.analytics.constants + +import com.google.firebase.analytics.FirebaseAnalytics + +internal object MoviesParams { + const val PARAM_DESTINATION = FirebaseAnalytics.Param.SCREEN_NAME + const val PARAM_ARGUMENTS = "destination_arguments" + + const val PARAM_SELECTED_LANGUAGE = "selected_language" + const val PARAM_SELECTED_THEME = "selected_theme" + const val PARAM_DYNAMIC_COLORS_ENABLED = "dynamic_colors_enabled" + const val PARAM_RTL_ENABLED = "rtl_enabled" +} \ No newline at end of file diff --git a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeDynamicColorsEvent.kt b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeDynamicColorsEvent.kt index 61761b1f6..dcc67aaa1 100644 --- a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeDynamicColorsEvent.kt +++ b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeDynamicColorsEvent.kt @@ -1,8 +1,8 @@ package org.michaelbel.movies.analytics.event -import org.michaelbel.movies.analytics.model.MoviesEvents +import org.michaelbel.movies.analytics.constants.MoviesEvents import org.michaelbel.movies.analytics.model.BaseEvent -import org.michaelbel.movies.analytics.model.MoviesParams +import org.michaelbel.movies.analytics.constants.MoviesParams class ChangeDynamicColorsEvent( enabled: Boolean diff --git a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeRtlEnabledEvent.kt b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeRtlEnabledEvent.kt index f877b498c..486a70fd5 100644 --- a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeRtlEnabledEvent.kt +++ b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/ChangeRtlEnabledEvent.kt @@ -1,8 +1,8 @@ package org.michaelbel.movies.analytics.event import org.michaelbel.movies.analytics.model.BaseEvent -import org.michaelbel.movies.analytics.model.MoviesEvents -import org.michaelbel.movies.analytics.model.MoviesParams +import org.michaelbel.movies.analytics.constants.MoviesEvents +import org.michaelbel.movies.analytics.constants.MoviesParams class ChangeRtlEnabledEvent( enabled: Boolean diff --git a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectLanguageEvent.kt b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectLanguageEvent.kt index e873066d2..c7c343e6f 100644 --- a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectLanguageEvent.kt +++ b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectLanguageEvent.kt @@ -1,8 +1,8 @@ package org.michaelbel.movies.analytics.event import org.michaelbel.movies.analytics.model.BaseEvent -import org.michaelbel.movies.analytics.model.MoviesEvents -import org.michaelbel.movies.analytics.model.MoviesParams +import org.michaelbel.movies.analytics.constants.MoviesEvents +import org.michaelbel.movies.analytics.constants.MoviesParams class SelectLanguageEvent( language: String diff --git a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectThemeEvent.kt b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectThemeEvent.kt index fca924713..c0f1af152 100644 --- a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectThemeEvent.kt +++ b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/event/SelectThemeEvent.kt @@ -1,8 +1,8 @@ package org.michaelbel.movies.analytics.event -import org.michaelbel.movies.analytics.model.MoviesEvents +import org.michaelbel.movies.analytics.constants.MoviesEvents import org.michaelbel.movies.analytics.model.BaseEvent -import org.michaelbel.movies.analytics.model.MoviesParams +import org.michaelbel.movies.analytics.constants.MoviesParams class SelectThemeEvent( theme: String diff --git a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/impl/MoviesAnalyticsImpl.kt b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/impl/MoviesAnalyticsImpl.kt index 771962169..077c94412 100644 --- a/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/impl/MoviesAnalyticsImpl.kt +++ b/core/analytics/src/main/kotlin/org/michaelbel/movies/analytics/impl/MoviesAnalyticsImpl.kt @@ -5,9 +5,9 @@ import androidx.core.os.bundleOf import com.google.firebase.analytics.FirebaseAnalytics import javax.inject.Inject import org.michaelbel.movies.analytics.MoviesAnalytics +import org.michaelbel.movies.analytics.constants.MoviesEvents +import org.michaelbel.movies.analytics.constants.MoviesParams import org.michaelbel.movies.analytics.model.BaseEvent -import org.michaelbel.movies.analytics.model.MoviesEvents -import org.michaelbel.movies.analytics.model.MoviesParams internal class MoviesAnalyticsImpl @Inject constructor( private val firebaseAnalytics: FirebaseAnalytics diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index ade02f098..fbd0d65d7 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -33,7 +33,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.kotlin.compiler.extension.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } lint { diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/coroutines/CoroutinesDispatchersModule.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/coroutines/di/CoroutinesDispatchersModule.kt similarity index 82% rename from core/common/src/main/kotlin/org/michaelbel/movies/common/coroutines/CoroutinesDispatchersModule.kt rename to core/common/src/main/kotlin/org/michaelbel/movies/common/coroutines/di/CoroutinesDispatchersModule.kt index b1aa8bec4..403e55500 100644 --- a/core/common/src/main/kotlin/org/michaelbel/movies/common/coroutines/CoroutinesDispatchersModule.kt +++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/coroutines/di/CoroutinesDispatchersModule.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.common.coroutines +package org.michaelbel.movies.common.coroutines.di import dagger.Module import dagger.Provides @@ -6,6 +6,8 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import org.michaelbel.movies.common.coroutines.Dispatcher +import org.michaelbel.movies.common.coroutines.MoviesDispatchers @Module @InstallIn(SingletonComponent::class) diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/PermissionKtx.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/PermissionKtx.kt deleted file mode 100644 index 811cc79ea..000000000 --- a/core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/PermissionKtx.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.michaelbel.movies.common.ktx - -import android.content.Context -import android.content.pm.PackageManager -import androidx.core.content.ContextCompat - -fun String.denied(context: Context): Boolean { - return ContextCompat.checkSelfPermission( - context, - this - ) == PackageManager.PERMISSION_DENIED -} \ No newline at end of file diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/notifications/NotificationsModule.kt b/core/common/src/main/kotlin/org/michaelbel/movies/common/notifications/NotificationsModule.kt deleted file mode 100644 index fad16aee3..000000000 --- a/core/common/src/main/kotlin/org/michaelbel/movies/common/notifications/NotificationsModule.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.michaelbel.movies.common.notifications - -import android.app.NotificationManager -import android.content.Context -import androidx.core.content.ContextCompat -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -internal object NotificationsModule { - - @Provides - fun provideNotificationService( - @ApplicationContext context: Context - ): NotificationManager { - return ContextCompat.getSystemService( - context, - NotificationManager::class.java - ) as NotificationManager - } -} \ 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 new file mode 100644 index 000000000..c81f74d79 --- /dev/null +++ b/core/common/src/main/kotlin/org/michaelbel/movies/common/usecase/UseCase.kt @@ -0,0 +1,25 @@ +package org.michaelbel.movies.common.usecase + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import org.michaelbel.movies.entities.Either +import timber.log.Timber + +abstract class UseCase( + private val coroutineDispatcher: CoroutineDispatcher +) { + + suspend operator fun invoke(parameters: P): Either { + return try { + withContext(coroutineDispatcher) { + execute(parameters).let { Either.Success(it) } + } + } catch (e: Exception) { + Timber.d(e) + Either.Failure(e) + } + } + + @Throws(RuntimeException::class) + protected abstract suspend fun execute(parameters: P): R +} \ No newline at end of file diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index eb983a488..30d9fb35f 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("movies-android-library") id("movies-android-hilt") + id("com.google.devtools.ksp") } android { @@ -19,6 +20,13 @@ android { } } + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + listOf( + "-opt-in=androidx.paging.ExperimentalPagingApi", + "-opt-in=kotlinx.serialization.ExperimentalSerializationApi" + ) + } + lint { quiet = true abortOnError = false @@ -34,4 +42,9 @@ dependencies { implementation(project(":core:entities")) implementation(project(":core:network")) implementation(libs.bundles.datastore) + implementation(libs.bundles.room) + api(libs.androidx.hilt.work) + kapt(libs.androidx.hilt.compiler) + api(libs.androidx.work.runtime.ktx) + ksp(libs.androidx.room.compiler) } \ No newline at end of file diff --git a/core/domain/src/main/AndroidManifest.xml b/core/domain/src/main/AndroidManifest.xml index 1d26c87a1..5cc9e93f0 100644 --- a/core/domain/src/main/AndroidManifest.xml +++ b/core/domain/src/main/AndroidManifest.xml @@ -1,2 +1,23 @@ - \ No newline at end of file + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/domain/src/main/assets/movies.json b/core/domain/src/main/assets/movies.json new file mode 100644 index 000000000..80abacaf9 --- /dev/null +++ b/core/domain/src/main/assets/movies.json @@ -0,0 +1,32 @@ +[ + { + "id": 505642, + "overview": "Queen Ramonda, Shuri, M’Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T’Challa’s death. As the Wakandans strive to embrace their next chapter, the heroes must band together with the help of War Dog Nakia and Everett Ross and forge a new path for the kingdom of Wakanda.", + "poster_path": "/sv1xJUazXeYqALzczSZ3O6nkH75.jpg", + "backdrop_path": "/xDMIl84Qo5Tsu62c9DGWhmPI67A.jpg", + "release_date": "2022-11-09", + "title": "Black Panther: Wakanda Forever", + "vote_average": 7.4, + "genre_ids": [28, 12, 878] + }, + { + "id": 640146, + "overview": "Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange new creatures and embarking on an adventure that will push them beyond the limits of what they thought possible.", + "poster_path": "/ngl2FKBlU4fhbdsrtdom9LVLBXw.jpg", + "backdrop_path": "/8YFL5QQVPy3AgrEQxNYVSgiPEbe.jpg", + "release_date": "2023-02-10", + "title": "Ant-Man and the Wasp: Quantumania", + "vote_average": 6.5, + "genre_ids": [12, 878, 35] + }, + { + "id": 49046, + "overview": "Paul Baumer and his friends Albert and Muller, egged on by romantic dreams of heroism, voluntarily enlist in the German army. Full of excitement and patriotic fervour, the boys enthusiastically march into a war they believe in. But once on the Western Front, they discover the soul-destroying horror of World War I.", + "poster_path": "/hYqOjJ7Gh1fbqXrxlIao1g8ZehF.jpg", + "backdrop_path": "/mqsPyyeDCBAghXyjbw4TfEYwljw.jpg", + "release_date": "2022-10-07", + "title": "All Quiet on the Western Front", + "vote_average": 7.8, + "genre_ids": [28, 18, 10752] + } +] \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/AppDatabase.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/AppDatabase.kt new file mode 100644 index 000000000..2fc141108 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/AppDatabase.kt @@ -0,0 +1,50 @@ +package org.michaelbel.movies.domain.data + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import org.michaelbel.movies.domain.data.converter.CalendarConverter +import org.michaelbel.movies.domain.data.dao.MovieDao +import org.michaelbel.movies.domain.data.dao.PagingKeyDao +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.domain.data.entity.PagingKeyDb + +/** + * The Room database for this app + */ +@Database( + entities = [ + MovieDb::class, + PagingKeyDb::class + ], + version = AppDatabase.DATABASE_VERSION, + exportSchema = false +) +@TypeConverters(CalendarConverter::class) +abstract class AppDatabase: RoomDatabase() { + + abstract fun movieDao(): MovieDao + abstract fun pagingKeyDao(): PagingKeyDao + + companion object { + private const val DATABASE_NAME = "movies-db" + const val DATABASE_VERSION = 13 + + @Volatile + private var instance: AppDatabase? = null + + fun getInstance(context: Context): AppDatabase { + return instance ?: synchronized(this) { + instance ?: buildDatabase(context).also { instance = it } + } + } + + private fun buildDatabase(context: Context): AppDatabase { + return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) + .fallbackToDestructiveMigration() + .build() + } + } +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/converter/CalendarConverter.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/converter/CalendarConverter.kt new file mode 100644 index 000000000..83c99c297 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/converter/CalendarConverter.kt @@ -0,0 +1,20 @@ +package org.michaelbel.movies.domain.data.converter + +import androidx.room.TypeConverter +import java.util.Calendar + +/** + * Type converters to allow Room to reference complex data types. + */ +internal object CalendarConverter { + + @TypeConverter + fun calendarToDatestamp(calendar: Calendar): Long { + return calendar.timeInMillis + } + + @TypeConverter + fun datestampToCalendar(value: Long): Calendar { + return Calendar.getInstance().apply { timeInMillis = value } + } +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/dao/MovieDao.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/dao/MovieDao.kt new file mode 100644 index 000000000..033ac26a0 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/dao/MovieDao.kt @@ -0,0 +1,33 @@ +package org.michaelbel.movies.domain.data.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import org.michaelbel.movies.domain.data.entity.MovieDb + +/** + * The Data Access Object for the [MovieDb] class. + */ +@Dao +interface MovieDao { + + @Query("SELECT * FROM movies WHERE movieList = :movieList ORDER BY position ASC") + fun pagingSource(movieList: String): PagingSource + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAllMovies(movies: List) + + @Query("DELETE FROM movies WHERE movieList = :movieList") + suspend fun removeAllMovies(movieList: String) + + @Query("SELECT * FROM movies WHERE id = :movieId") + suspend fun movieById(movieId: Int): MovieDb? + + @Query("SELECT MAX(position) from movies WHERE movieList = :movieList") + suspend fun maxPosition(movieList: String): Int? + + @Query("SELECT COUNT(*) FROM movies WHERE movieList = :movieList") + suspend fun moviesCount(movieList: String): Int +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/dao/PagingKeyDao.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/dao/PagingKeyDao.kt new file mode 100644 index 000000000..3ee023d4f --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/dao/PagingKeyDao.kt @@ -0,0 +1,23 @@ +package org.michaelbel.movies.domain.data.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import org.michaelbel.movies.domain.data.entity.PagingKeyDb + +/** + * The Data Access Object for the [PagingKeyDb] class. + */ +@Dao +interface PagingKeyDao { + + @Query("SELECT * FROM pagingkeys WHERE movieList = :movieList") + suspend fun pagingKey(movieList: String): PagingKeyDb? + + @Query("DELETE FROM pagingkeys WHERE movieList = :movieList") + suspend fun removePagingKey(movieList: String) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertPagingKey(pagingKey: PagingKeyDb) +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/dao/ktx/MovieDaoKtx.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/dao/ktx/MovieDaoKtx.kt new file mode 100644 index 000000000..ec8a25ad1 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/dao/ktx/MovieDaoKtx.kt @@ -0,0 +1,7 @@ +package org.michaelbel.movies.domain.data.dao.ktx + +import org.michaelbel.movies.domain.data.dao.MovieDao + +internal suspend fun MovieDao.isEmpty(movieList: String): Boolean { + return moviesCount(movieList) == 0 +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/di/DatabaseModule.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/di/DatabaseModule.kt new file mode 100644 index 000000000..4bf7c92c5 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/di/DatabaseModule.kt @@ -0,0 +1,29 @@ +package org.michaelbel.movies.domain.data.di + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import org.michaelbel.movies.domain.data.AppDatabase +import org.michaelbel.movies.domain.data.dao.MovieDao +import org.michaelbel.movies.domain.data.dao.PagingKeyDao +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object DatabaseModule { + + @Provides + @Singleton + fun provideAppDatabase( + @ApplicationContext context: Context + ): AppDatabase = AppDatabase.getInstance(context) + + @Provides + fun provideMovieDao(appDatabase: AppDatabase): MovieDao = appDatabase.movieDao() + + @Provides + fun providePagingKeyDao(appDatabase: AppDatabase): PagingKeyDao = appDatabase.pagingKeyDao() +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/entity/MovieDb.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/entity/MovieDb.kt new file mode 100644 index 000000000..be975d96f --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/entity/MovieDb.kt @@ -0,0 +1,37 @@ +package org.michaelbel.movies.domain.data.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.jetbrains.annotations.NotNull + +@Entity(tableName = "movies") +data class MovieDb( + @NotNull val movieList: String, + @NotNull val dateAdded: Long, + @NotNull val position: Int, + @NotNull @PrimaryKey @ColumnInfo(name = "id") val movieId: Int, + @NotNull val overview: String, + @NotNull val posterPath: String, + @NotNull val backdropPath: String, + @NotNull val releaseDate: String, + @NotNull val title: String, + @NotNull val voteAverage: Float +) { + companion object { + const val MOVIES_LOCAL_LIST = "movies_local" + + val Empty: MovieDb = MovieDb( + movieList = "", + dateAdded = 0L, + position = 0, + movieId = 0, + overview = "", + posterPath = "", + backdropPath = "", + releaseDate = "", + title = "", + voteAverage = 0F + ) + } +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/entity/PagingKeyDb.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/entity/PagingKeyDb.kt new file mode 100644 index 000000000..9e10e296f --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/entity/PagingKeyDb.kt @@ -0,0 +1,10 @@ +package org.michaelbel.movies.domain.data.entity + +import androidx.room.Entity +import org.jetbrains.annotations.NotNull + +@Entity(tableName = "pagingkeys", primaryKeys = ["movieList"]) +data class PagingKeyDb( + @NotNull val movieList: String, + val page: Int? = null +) \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/ktx/MovieDbKtx.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/ktx/MovieDbKtx.kt new file mode 100644 index 000000000..e5e7643bd --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/data/ktx/MovieDbKtx.kt @@ -0,0 +1,11 @@ +package org.michaelbel.movies.domain.data.ktx + +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.entities.TMDB_MOVIE_URL +import java.util.Locale + +val MovieDb.isNotEmpty: Boolean + get() = this != MovieDb.Empty + +val MovieDb.url: String + get() = String.format(Locale.US, TMDB_MOVIE_URL, movieId) \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/di/DataSourceModule.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/di/DataSourceModule.kt deleted file mode 100644 index c0978d4d2..000000000 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/di/DataSourceModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.michaelbel.movies.domain.datasource.di - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton -import org.michaelbel.movies.domain.datasource.MovieNetwork -import org.michaelbel.movies.domain.datasource.ktx.createService -import retrofit2.Retrofit - -@Module -@InstallIn(SingletonComponent::class) -internal object DataSourceModule { - - @Provides - @Singleton - fun provideMovieApi(retrofit: Retrofit): MovieNetwork = retrofit.createService() -} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/ktx/DataStore.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/ktx/DataStore.kt deleted file mode 100644 index 40447fd48..000000000 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/ktx/DataStore.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.michaelbel.movies.domain.datasource.ktx - -import android.content.Context -import androidx.datastore.preferences.SharedPreferencesMigration -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.preferencesDataStore - -private const val USER_PREFERENCES_NAME = "user_preferences" - -internal val PREFERENCE_THEME_KEY = intPreferencesKey("theme") -internal val PREFERENCE_DYNAMIC_COLORS_KEY = booleanPreferencesKey("dynamic_colors") -internal val PREFERENCE_RTL_ENABLED_KEY = booleanPreferencesKey("rtl_enabled") -internal val PREFERENCE_NETWORK_REQUEST_DELAY = intPreferencesKey("network_request_delay") - -internal val Context.dataStore by preferencesDataStore( - name = USER_PREFERENCES_NAME, - produceMigrations = { context -> - listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME)) - } -) \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/ktx/RetrofitKtx.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/ktx/RetrofitKtx.kt deleted file mode 100644 index e39c613e9..000000000 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/ktx/RetrofitKtx.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.michaelbel.movies.domain.datasource.ktx - -import retrofit2.Retrofit - -inline fun Retrofit.createService(): T { - return create(T::class.java) -} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/exceptions/ktx/ApiKeyNotNullExceptionKtx.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/exceptions/ktx/ApiKeyNotNullExceptionKtx.kt index 6cea8ac44..cc0fd1fc6 100644 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/exceptions/ktx/ApiKeyNotNullExceptionKtx.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/exceptions/ktx/ApiKeyNotNullExceptionKtx.kt @@ -1,8 +1,8 @@ package org.michaelbel.movies.domain.exceptions.ktx import org.michaelbel.movies.domain.exceptions.ApiKeyNotNullException -import org.michaelbel.movies.entities.tmdbApiKey +import org.michaelbel.movies.entities.isTmdbApiKeyEmpty internal fun checkApiKeyNotNullException() { - if (tmdbApiKey.isEmpty() || tmdbApiKey == "null") throw ApiKeyNotNullException + if (isTmdbApiKeyEmpty) throw ApiKeyNotNullException } \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/MovieInteractor.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/MovieInteractor.kt deleted file mode 100644 index 1c877537f..000000000 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/MovieInteractor.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.michaelbel.movies.domain.interactor - -import org.michaelbel.movies.entities.Either -import org.michaelbel.movies.entities.MovieData -import org.michaelbel.movies.entities.MovieDetailsData - -interface MovieInteractor { - - suspend fun movieList(list: String, page: Int): Pair, Int> - - suspend fun movieDetails(movieId: Long): Either -} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/di/InteractorModule.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/di/InteractorModule.kt index 450e8e124..758a0c6fc 100644 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/di/InteractorModule.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/di/InteractorModule.kt @@ -4,11 +4,11 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import org.michaelbel.movies.domain.interactor.movie.MovieInteractor +import org.michaelbel.movies.domain.interactor.movie.impl.MovieInteractorImpl +import org.michaelbel.movies.domain.interactor.settings.SettingsInteractor +import org.michaelbel.movies.domain.interactor.settings.impl.SettingsInteractorImpl import javax.inject.Singleton -import org.michaelbel.movies.domain.interactor.MovieInteractor -import org.michaelbel.movies.domain.interactor.SettingsInteractor -import org.michaelbel.movies.domain.interactor.impl.MovieInteractorImpl -import org.michaelbel.movies.domain.interactor.impl.SettingsInteractorImpl @Module @InstallIn(SingletonComponent::class) diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/impl/MovieInteractorImpl.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/impl/MovieInteractorImpl.kt deleted file mode 100644 index 1ad7746c6..000000000 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/impl/MovieInteractorImpl.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.michaelbel.movies.domain.interactor.impl - -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext -import org.michaelbel.movies.common.coroutines.Dispatcher -import org.michaelbel.movies.common.coroutines.MoviesDispatchers -import org.michaelbel.movies.domain.interactor.MovieInteractor -import org.michaelbel.movies.domain.interactor.SettingsInteractor -import org.michaelbel.movies.domain.repository.MovieRepository -import org.michaelbel.movies.entities.Either -import org.michaelbel.movies.entities.MovieData -import org.michaelbel.movies.entities.MovieDetailsData - -internal class MovieInteractorImpl @Inject constructor( - @Dispatcher(MoviesDispatchers.IO) private val dispatcher: CoroutineDispatcher, - private val movieRepository: MovieRepository, - private val settingsInteractor: SettingsInteractor -): MovieInteractor { - - override suspend fun movieList(list: String, page: Int): Pair, Int> { - delay(settingsInteractor.networkRequestDelay()) - - return withContext(dispatcher) { - movieRepository.movieList(list, page) - } - } - - override suspend fun movieDetails(movieId: Long): Either { - delay(settingsInteractor.networkRequestDelay()) - - return withContext(dispatcher) { - movieRepository.movieDetails(movieId) - } - } -} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/movie/MovieInteractor.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/movie/MovieInteractor.kt new file mode 100644 index 000000000..9e67701cb --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/movie/MovieInteractor.kt @@ -0,0 +1,26 @@ +package org.michaelbel.movies.domain.interactor.movie + +import androidx.paging.PagingSource +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.entities.Either +import org.michaelbel.movies.network.model.MovieResponse +import org.michaelbel.movies.network.model.Result + +interface MovieInteractor { + + fun moviesPagingSource(movieList: String): PagingSource + + suspend fun moviesResult(movieList: String, page: Int): Result + + suspend fun movieDetails(movieId: Int): Either + + suspend fun removeAllMovies(movieList: String) + + suspend fun insertAllMovies(movieList: String, movies: List) + + suspend fun page(movieList: String): Int? + + suspend fun removePagingKey(movieList: String) + + suspend fun insertPagingKey(movieList: String, page: Int) +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/movie/impl/MovieInteractorImpl.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/movie/impl/MovieInteractorImpl.kt new file mode 100644 index 000000000..6e7c0c074 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/movie/impl/MovieInteractorImpl.kt @@ -0,0 +1,75 @@ +package org.michaelbel.movies.domain.interactor.movie.impl + +import androidx.paging.PagingSource +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import org.michaelbel.movies.common.coroutines.Dispatcher +import org.michaelbel.movies.common.coroutines.MoviesDispatchers +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.domain.interactor.movie.MovieInteractor +import org.michaelbel.movies.domain.repository.movie.MovieRepository +import org.michaelbel.movies.domain.usecase.DelayUseCase +import org.michaelbel.movies.entities.Either +import org.michaelbel.movies.network.model.MovieResponse +import org.michaelbel.movies.network.model.Result + +@Singleton +internal class MovieInteractorImpl @Inject constructor( + @Dispatcher(MoviesDispatchers.IO) private val dispatcher: CoroutineDispatcher, + private val movieRepository: MovieRepository, + private val delayUseCase: DelayUseCase +): MovieInteractor { + + override fun moviesPagingSource(movieList: String): PagingSource { + return movieRepository.moviesPagingSource(movieList) + } + + override suspend fun moviesResult(movieList: String, page: Int): Result { + delay(delayUseCase.networkRequestDelay()) + + return withContext(dispatcher) { + movieRepository.moviesResult(movieList, page) + } + } + + override suspend fun movieDetails(movieId: Int): Either { + delay(delayUseCase.networkRequestDelay()) + + return withContext(dispatcher) { + movieRepository.movieDetails(movieId) + } + } + + override suspend fun removeAllMovies(movieList: String) { + return withContext(dispatcher) { + movieRepository.removeAllMovies(movieList) + } + } + + override suspend fun insertAllMovies(movieList: String, movies: List) { + return withContext(dispatcher) { + movieRepository.insertAllMovies(movieList, movies) + } + } + + override suspend fun page(movieList: String): Int? { + return withContext(dispatcher) { + movieRepository.page(movieList) + } + } + + override suspend fun removePagingKey(movieList: String) { + return withContext(dispatcher) { + movieRepository.removePagingKey(movieList) + } + } + + override suspend fun insertPagingKey(movieList: String, page: Int) { + return withContext(dispatcher) { + movieRepository.insertPagingKey(movieList, page) + } + } +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/SettingsInteractor.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/settings/SettingsInteractor.kt similarity index 75% rename from core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/SettingsInteractor.kt rename to core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/settings/SettingsInteractor.kt index 8b4a4ef3b..6415be40b 100644 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/SettingsInteractor.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/settings/SettingsInteractor.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.domain.interactor +package org.michaelbel.movies.domain.interactor.settings import androidx.compose.ui.unit.LayoutDirection import kotlinx.coroutines.flow.Flow @@ -13,27 +13,19 @@ interface SettingsInteractor { val layoutDirection: Flow - val areNotificationsEnabled: Boolean - val isSettingsIconVisible: Flow val isPlayServicesAvailable: Flow val isAppFromGooglePlay: Flow - val networkRequestDelay: Flow - val appVersionData: Flow - suspend fun networkRequestDelay(): Long - suspend fun selectTheme(theme: AppTheme) suspend fun setDynamicColors(value: Boolean) suspend fun setRtlEnabled(value: Boolean) - suspend fun setNetworkRequestDelay(value: Int) - suspend fun fetchRemoteConfig() } \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/impl/SettingsInteractorImpl.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/settings/impl/SettingsInteractorImpl.kt similarity index 77% rename from core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/impl/SettingsInteractorImpl.kt rename to core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/settings/impl/SettingsInteractorImpl.kt index 532380ccb..6ac4baa2b 100644 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/impl/SettingsInteractorImpl.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/interactor/settings/impl/SettingsInteractorImpl.kt @@ -1,10 +1,7 @@ -package org.michaelbel.movies.domain.interactor.impl +package org.michaelbel.movies.domain.interactor.settings.impl -import android.app.NotificationManager -import android.os.Build import androidx.compose.ui.unit.LayoutDirection import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf @@ -17,16 +14,18 @@ import org.michaelbel.movies.common.config.RemoteParams import org.michaelbel.movies.common.coroutines.Dispatcher import org.michaelbel.movies.common.coroutines.MoviesDispatchers import org.michaelbel.movies.common.googleapi.GoogleApi -import org.michaelbel.movies.domain.interactor.SettingsInteractor -import org.michaelbel.movies.domain.repository.SettingsRepository +import org.michaelbel.movies.domain.interactor.settings.SettingsInteractor +import org.michaelbel.movies.domain.repository.settings.SettingsRepository import org.michaelbel.movies.ui.theme.model.AppTheme import org.michaelbel.movies.ui.version.AppVersionData import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton +@Singleton internal class SettingsInteractorImpl @Inject constructor( @Dispatcher(MoviesDispatchers.Main) private val dispatcher: CoroutineDispatcher, private val settingsRepository: SettingsRepository, - notificationManager: NotificationManager, private val firebaseRemoteConfig: FirebaseRemoteConfig, googleApi: GoogleApi, private val analytics: MoviesAnalytics @@ -38,14 +37,6 @@ internal class SettingsInteractorImpl @Inject constructor( override val layoutDirection: Flow = settingsRepository.layoutDirection - override val networkRequestDelay: Flow = settingsRepository.networkRequestDelay - - override val areNotificationsEnabled: Boolean = if (Build.VERSION.SDK_INT >= 24) { - notificationManager.areNotificationsEnabled() - } else { - true - } - override val isSettingsIconVisible: Flow = flowOf( firebaseRemoteConfig.getBoolean(RemoteParams.PARAM_SETTINGS_ICON_VISIBLE) ) @@ -56,10 +47,6 @@ internal class SettingsInteractorImpl @Inject constructor( override val appVersionData: Flow = settingsRepository.appVersionData - override suspend fun networkRequestDelay(): Long { - return settingsRepository.networkRequestDelay() - } - override suspend fun selectTheme(theme: AppTheme) = withContext(dispatcher) { settingsRepository.selectTheme(theme) analytics.logEvent(SelectThemeEvent(theme.toString())) @@ -75,10 +62,6 @@ internal class SettingsInteractorImpl @Inject constructor( analytics.logEvent(ChangeRtlEnabledEvent(value)) } - override suspend fun setNetworkRequestDelay(value: Int) = withContext(dispatcher) { - settingsRepository.setNetworkRequestDelay(value) - } - override suspend fun fetchRemoteConfig() { withContext(dispatcher) { firebaseRemoteConfig diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/MovieKtx.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/MovieKtx.kt new file mode 100644 index 000000000..916066391 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/MovieKtx.kt @@ -0,0 +1,18 @@ +package org.michaelbel.movies.domain.ktx + +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.network.model.Movie + +internal val Movie.mapToMovieDb: MovieDb + get() = MovieDb( + movieList = "", + dateAdded = 0L, + position = 0, + movieId = id, + overview = overview.orEmpty(), + posterPath = posterPath.orEmpty(), + backdropPath = formatImageUrl(backdropPath.orEmpty()), + releaseDate = releaseDate.orEmpty(), + title = title.orEmpty(), + voteAverage = voteAverage + ) \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/MovieResponseKtx.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/MovieResponseKtx.kt new file mode 100644 index 000000000..5fa505f34 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/MovieResponseKtx.kt @@ -0,0 +1,38 @@ +package org.michaelbel.movies.domain.ktx + +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.network.model.MovieResponse +import java.util.Locale + +private const val TMDB_IMAGE_BASE_URL = "https://image.tmdb.org/t/p/%s/%s" +private const val TMDB_IMAGE_FILE_SIZE_ORIGINAL = "original" +private const val TMDB_IMAGE_FILE_SIZE_W500 = "w500" +private const val IMAGE_EMPTY_URL = "https://null" + +private val feedFileSize: String by lazy { + String.format(Locale.ENGLISH, TMDB_IMAGE_FILE_SIZE_W500, TMDB_IMAGE_FILE_SIZE_W500) +} + +fun formatImageUrl(path: String, size: String = feedFileSize): String { + return if (path.isNotEmpty()) { + val imageSize: String = size.ifEmpty { TMDB_IMAGE_FILE_SIZE_ORIGINAL } + String.format(Locale.ENGLISH, TMDB_IMAGE_BASE_URL, imageSize, path) + } else { + IMAGE_EMPTY_URL + } +} + +internal fun MovieResponse.mapToMovieDb(movieList: String, position: Int): MovieDb { + return MovieDb( + movieList = movieList, + dateAdded = System.currentTimeMillis(), + position = position, + movieId = id, + overview = overview.orEmpty(), + posterPath = posterPath.orEmpty(), + backdropPath = formatImageUrl(backdropPath.orEmpty()), + releaseDate = releaseDate, + title = title, + voteAverage = voteAverage + ) +} \ No newline at end of file diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/PackageInfoKtx.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/PackageInfoKtx.kt similarity index 93% rename from core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/PackageInfoKtx.kt rename to core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/PackageInfoKtx.kt index 2719f64d3..f7f68ca9c 100644 --- a/core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/PackageInfoKtx.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/ktx/PackageInfoKtx.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.common.ktx +package org.michaelbel.movies.domain.ktx import android.content.Context import android.content.pm.PackageInfo diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/mediator/MoviesRemoteMediator.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/mediator/MoviesRemoteMediator.kt new file mode 100644 index 000000000..3519feb5c --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/mediator/MoviesRemoteMediator.kt @@ -0,0 +1,62 @@ +package org.michaelbel.movies.domain.mediator + +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.withTransaction +import org.michaelbel.movies.domain.data.AppDatabase +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.domain.interactor.movie.MovieInteractor +import org.michaelbel.movies.network.ktx.isPaginationReached +import org.michaelbel.movies.network.ktx.nextPage +import org.michaelbel.movies.network.model.MovieResponse +import org.michaelbel.movies.network.model.Result + +class MoviesRemoteMediator( + private val appDatabase: AppDatabase, + private val movieInteractor: MovieInteractor, + private val movieList: String +): RemoteMediator() { + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): MediatorResult { + val reachedResult: MediatorResult = MediatorResult.Success(endOfPaginationReached = true) + return try { + val loadKey: Int? = when (loadType) { + LoadType.REFRESH -> { + null + } + LoadType.PREPEND -> { + return reachedResult + } + LoadType.APPEND -> { + movieInteractor.page(movieList) ?: return reachedResult + } + } + + val moviesResult: Result = movieInteractor.moviesResult( + movieList = movieList, + page = loadKey ?: 1 + ) + + appDatabase.withTransaction { + if (loadType == LoadType.REFRESH) { + movieInteractor.run { + removePagingKey(movieList) + removeAllMovies(movieList) + } + } + movieInteractor.run { + insertPagingKey(movieList, moviesResult.nextPage) + insertAllMovies(movieList, moviesResult.results) + } + } + + MediatorResult.Success(endOfPaginationReached = moviesResult.isPaginationReached) + } catch (e: Exception) { + MediatorResult.Error(e) + } + } +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/preferences/constants/Constants.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/preferences/constants/Constants.kt new file mode 100644 index 000000000..63553f7ab --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/preferences/constants/Constants.kt @@ -0,0 +1,12 @@ +package org.michaelbel.movies.domain.preferences.constants + +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.intPreferencesKey + +internal const val USER_PREFERENCES_NAME = "user_preferences" + +internal val PREFERENCE_THEME_KEY: Preferences.Key = intPreferencesKey("theme") +internal val PREFERENCE_DYNAMIC_COLORS_KEY: Preferences.Key = booleanPreferencesKey("dynamic_colors") +internal val PREFERENCE_RTL_ENABLED_KEY: Preferences.Key = booleanPreferencesKey("rtl_enabled") +internal val PREFERENCE_NETWORK_REQUEST_DELAY: Preferences.Key = intPreferencesKey("network_request_delay") \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/di/PreferencesModule.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/preferences/di/DataStoreModule.kt similarity index 78% rename from core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/di/PreferencesModule.kt rename to core/domain/src/main/kotlin/org/michaelbel/movies/domain/preferences/di/DataStoreModule.kt index 83635208b..95fb61547 100644 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/di/PreferencesModule.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/preferences/di/DataStoreModule.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.domain.datasource.di +package org.michaelbel.movies.domain.preferences.di import android.content.Context import androidx.datastore.core.DataStore @@ -8,12 +8,12 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import org.michaelbel.movies.domain.preferences.ktx.dataStore import javax.inject.Singleton -import org.michaelbel.movies.domain.datasource.ktx.dataStore @Module @InstallIn(SingletonComponent::class) -internal object PreferencesModule { +internal object DataStoreModule { @Provides @Singleton diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/preferences/ktx/DataStoreKtx.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/preferences/ktx/DataStoreKtx.kt new file mode 100644 index 000000000..e7b127e16 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/preferences/ktx/DataStoreKtx.kt @@ -0,0 +1,13 @@ +package org.michaelbel.movies.domain.preferences.ktx + +import android.content.Context +import androidx.datastore.preferences.SharedPreferencesMigration +import androidx.datastore.preferences.preferencesDataStore +import org.michaelbel.movies.domain.preferences.constants.USER_PREFERENCES_NAME + +internal val Context.dataStore by preferencesDataStore( + name = USER_PREFERENCES_NAME, + produceMigrations = { context -> + listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME)) + } +) \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/MovieRepository.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/MovieRepository.kt deleted file mode 100644 index 3044434b7..000000000 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/MovieRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.michaelbel.movies.domain.repository - -import org.michaelbel.movies.entities.Either -import org.michaelbel.movies.entities.MovieData -import org.michaelbel.movies.entities.MovieDetailsData - -interface MovieRepository { - - suspend fun movieList(list: String, page: Int): Pair, Int> - - suspend fun movieDetails(movieId: Long): Either -} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/di/RepositoryModule.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/di/RepositoryModule.kt index 3bfa6c988..d495c3251 100644 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/di/RepositoryModule.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/di/RepositoryModule.kt @@ -5,10 +5,10 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton -import org.michaelbel.movies.domain.repository.MovieRepository -import org.michaelbel.movies.domain.repository.SettingsRepository -import org.michaelbel.movies.domain.repository.impl.MovieRepositoryImpl -import org.michaelbel.movies.domain.repository.impl.SettingsRepositoryImpl +import org.michaelbel.movies.domain.repository.movie.MovieRepository +import org.michaelbel.movies.domain.repository.settings.SettingsRepository +import org.michaelbel.movies.domain.repository.movie.impl.MovieRepositoryImpl +import org.michaelbel.movies.domain.repository.settings.impl.SettingsRepositoryImpl @Module @InstallIn(SingletonComponent::class) diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/impl/MovieRepositoryImpl.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/impl/MovieRepositoryImpl.kt deleted file mode 100644 index 01b558924..000000000 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/impl/MovieRepositoryImpl.kt +++ /dev/null @@ -1,46 +0,0 @@ -package org.michaelbel.movies.domain.repository.impl - -import javax.inject.Inject -import org.michaelbel.movies.common.localization.LocaleController -import org.michaelbel.movies.domain.datasource.MovieNetwork -import org.michaelbel.movies.domain.exceptions.ktx.checkApiKeyNotNullException -import org.michaelbel.movies.domain.repository.MovieRepository -import org.michaelbel.movies.entities.Either -import org.michaelbel.movies.entities.MovieData -import org.michaelbel.movies.entities.MovieDetailsData -import org.michaelbel.movies.entities.mapper.MovieMapper -import org.michaelbel.movies.entities.response -import org.michaelbel.movies.entities.tmdbApiKey -import org.michaelbel.movies.network.model.Movie -import org.michaelbel.movies.network.model.MovieResponse -import org.michaelbel.movies.network.model.Result - -internal class MovieRepositoryImpl @Inject constructor( - private val movieApi: MovieNetwork, - private val movieMapper: MovieMapper, - private val localeController: LocaleController -): MovieRepository { - - override suspend fun movieList(list: String, page: Int): Pair, Int> { - checkApiKeyNotNullException() - - val result: Result = movieApi.movies( - list = list, - apiKey = tmdbApiKey, - language = localeController.language, - page = page - ) - return movieMapper.mapToMovieDataList(result.results) to result.totalPages - } - - override suspend fun movieDetails(movieId: Long): Either { - return response { - val movie: Movie = movieApi.movie( - id = movieId, - apiKey = tmdbApiKey, - language = localeController.language - ) - movieMapper.mapToMovieDetailsData(movie) - } - } -} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/movie/MovieRepository.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/movie/MovieRepository.kt new file mode 100644 index 000000000..a377c2cf7 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/movie/MovieRepository.kt @@ -0,0 +1,26 @@ +package org.michaelbel.movies.domain.repository.movie + +import androidx.paging.PagingSource +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.entities.Either +import org.michaelbel.movies.network.model.MovieResponse +import org.michaelbel.movies.network.model.Result + +interface MovieRepository { + + fun moviesPagingSource(movieList: String): PagingSource + + suspend fun moviesResult(movieList: String, page: Int): Result + + suspend fun movieDetails(movieId: Int): Either + + suspend fun removeAllMovies(movieList: String) + + suspend fun insertAllMovies(movieList: String, movies: List) + + suspend fun page(movieList: String): Int? + + suspend fun removePagingKey(movieList: String) + + suspend fun insertPagingKey(movieList: String, page: Int) +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/movie/impl/MovieRepositoryImpl.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/movie/impl/MovieRepositoryImpl.kt new file mode 100644 index 000000000..fd558681e --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/movie/impl/MovieRepositoryImpl.kt @@ -0,0 +1,97 @@ +package org.michaelbel.movies.domain.repository.movie.impl + +import androidx.paging.PagingSource +import org.michaelbel.movies.common.localization.LocaleController +import org.michaelbel.movies.domain.data.dao.MovieDao +import org.michaelbel.movies.domain.data.dao.PagingKeyDao +import org.michaelbel.movies.domain.data.dao.ktx.isEmpty +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.domain.data.entity.PagingKeyDb +import org.michaelbel.movies.domain.exceptions.ktx.checkApiKeyNotNullException +import org.michaelbel.movies.domain.ktx.mapToMovieDb +import org.michaelbel.movies.domain.repository.movie.MovieRepository +import org.michaelbel.movies.domain.service.movie.MovieService +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.Movie +import org.michaelbel.movies.network.model.MovieResponse +import org.michaelbel.movies.network.model.Result +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class MovieRepositoryImpl @Inject constructor( + private val movieApi: MovieService, + private val movieDao: MovieDao, + private val pagingKeyDao: PagingKeyDao, + private val localeController: LocaleController +): MovieRepository { + + override fun moviesPagingSource(movieList: String): PagingSource { + return movieDao.pagingSource(movieList) + } + + override suspend fun moviesResult(movieList: String, page: Int): Result { + if (isTmdbApiKeyEmpty && movieDao.isEmpty(MovieDb.MOVIES_LOCAL_LIST)) { + checkApiKeyNotNullException() + } + + return movieApi.movies( + list = movieList, + apiKey = tmdbApiKey, + language = localeController.language, + page = page + ) + } + + override suspend fun movieDetails(movieId: Int): Either { + return response { + val movieDb: MovieDb? = movieDao.movieById(movieId) + + if (movieDb != null) { + movieDb + } else { + val movie: Movie = movieApi.movie( + id = movieId, + apiKey = tmdbApiKey, + language = localeController.language + ) + movie.mapToMovieDb + } + } + } + + override suspend fun removeAllMovies(movieList: String) { + movieDao.removeAllMovies(movieList) + } + + override suspend fun insertAllMovies(movieList: String, movies: List) { + val maxPosition: Int = movieDao.maxPosition(movieList) ?: 0 + val moviesDb: List = movies.mapIndexed { index, movieResponse -> + movieResponse.mapToMovieDb( + movieList = movieList, + position = maxPosition.plus(index).plus(1) + ) + } + movieDao.insertAllMovies(moviesDb) + } + + override suspend fun page(movieList: String): Int? { + return pagingKeyDao.pagingKey(movieList)?.page + } + + override suspend fun removePagingKey(movieList: String) { + pagingKeyDao.removePagingKey(movieList) + } + + override suspend fun insertPagingKey(movieList: String, page: Int) { + pagingKeyDao.insertPagingKey( + PagingKeyDb( + movieList = movieList, + page = page + ) + ) + } +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/SettingsRepository.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/settings/SettingsRepository.kt similarity index 74% rename from core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/SettingsRepository.kt rename to core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/settings/SettingsRepository.kt index f76da8e41..6674134b9 100644 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/SettingsRepository.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/settings/SettingsRepository.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.domain.repository +package org.michaelbel.movies.domain.repository.settings import androidx.compose.ui.unit.LayoutDirection import kotlinx.coroutines.flow.Flow @@ -13,17 +13,11 @@ interface SettingsRepository { val layoutDirection: Flow - val networkRequestDelay: Flow - val appVersionData: Flow - suspend fun networkRequestDelay(): Long - suspend fun selectTheme(theme: AppTheme) suspend fun setDynamicColors(value: Boolean) suspend fun setRtlEnabled(value: Boolean) - - suspend fun setNetworkRequestDelay(value: Int) } \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/impl/SettingsRepositoryImpl.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/settings/impl/SettingsRepositoryImpl.kt similarity index 66% rename from core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/impl/SettingsRepositoryImpl.kt rename to core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/settings/impl/SettingsRepositoryImpl.kt index b0aeefb03..94082980d 100644 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/impl/SettingsRepositoryImpl.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/repository/settings/impl/SettingsRepositoryImpl.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.domain.repository.impl +package org.michaelbel.movies.domain.repository.settings.impl import android.content.Context import android.os.Build @@ -7,21 +7,21 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import org.michaelbel.movies.common.ktx.code -import org.michaelbel.movies.common.ktx.packageInfo -import org.michaelbel.movies.domain.datasource.ktx.PREFERENCE_DYNAMIC_COLORS_KEY -import org.michaelbel.movies.domain.datasource.ktx.PREFERENCE_NETWORK_REQUEST_DELAY -import org.michaelbel.movies.domain.datasource.ktx.PREFERENCE_RTL_ENABLED_KEY -import org.michaelbel.movies.domain.datasource.ktx.PREFERENCE_THEME_KEY -import org.michaelbel.movies.domain.repository.SettingsRepository +import org.michaelbel.movies.domain.ktx.code +import org.michaelbel.movies.domain.ktx.packageInfo +import org.michaelbel.movies.domain.preferences.constants.PREFERENCE_DYNAMIC_COLORS_KEY +import org.michaelbel.movies.domain.preferences.constants.PREFERENCE_RTL_ENABLED_KEY +import org.michaelbel.movies.domain.preferences.constants.PREFERENCE_THEME_KEY +import org.michaelbel.movies.domain.repository.settings.SettingsRepository import org.michaelbel.movies.ui.theme.model.AppTheme import org.michaelbel.movies.ui.version.AppVersionData +import javax.inject.Inject +import javax.inject.Singleton +@Singleton internal class SettingsRepositoryImpl @Inject constructor( @ApplicationContext private val context: Context, private val dataStore: DataStore @@ -40,10 +40,6 @@ internal class SettingsRepositoryImpl @Inject constructor( if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr } - override val networkRequestDelay: Flow = dataStore.data.map { preferences -> - preferences[PREFERENCE_NETWORK_REQUEST_DELAY] ?: 0 - } - override val appVersionData: Flow = flowOf( AppVersionData( version = context.packageInfo.versionName, @@ -51,10 +47,6 @@ internal class SettingsRepositoryImpl @Inject constructor( ) ) - override suspend fun networkRequestDelay(): Long { - return dataStore.data.first()[PREFERENCE_NETWORK_REQUEST_DELAY]?.toLong() ?: 0L - } - override suspend fun selectTheme(theme: AppTheme) { dataStore.edit { preferences -> preferences[PREFERENCE_THEME_KEY] = theme.theme @@ -72,10 +64,4 @@ internal class SettingsRepositoryImpl @Inject constructor( preferences[PREFERENCE_RTL_ENABLED_KEY] = value } } - - override suspend fun setNetworkRequestDelay(value: Int) { - dataStore.edit { preferences -> - preferences[PREFERENCE_NETWORK_REQUEST_DELAY] = value - } - } } \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/service/di/ServiceModule.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/service/di/ServiceModule.kt new file mode 100644 index 000000000..5e606d185 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/service/di/ServiceModule.kt @@ -0,0 +1,19 @@ +package org.michaelbel.movies.domain.service.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.michaelbel.movies.domain.service.movie.MovieService +import org.michaelbel.movies.domain.service.ktx.createService +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object ServiceModule { + + @Provides + @Singleton + fun provideMovieService(retrofit: Retrofit): MovieService = retrofit.createService() +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/service/ktx/RetrofitKtx.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/service/ktx/RetrofitKtx.kt new file mode 100644 index 000000000..5c2799644 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/service/ktx/RetrofitKtx.kt @@ -0,0 +1,5 @@ +package org.michaelbel.movies.domain.service.ktx + +import retrofit2.Retrofit + +inline fun Retrofit.createService(): T = create(T::class.java) \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/MovieNetwork.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/service/movie/MovieService.kt similarity index 83% rename from core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/MovieNetwork.kt rename to core/domain/src/main/kotlin/org/michaelbel/movies/domain/service/movie/MovieService.kt index ad7e5d320..51dfd7d22 100644 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/datasource/MovieNetwork.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/service/movie/MovieService.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.domain.datasource +package org.michaelbel.movies.domain.service.movie import org.michaelbel.movies.network.model.Movie import org.michaelbel.movies.network.model.MovieResponse @@ -7,11 +7,11 @@ import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query -internal interface MovieNetwork { +internal interface MovieService { @GET("movie/{movie_id}") suspend fun movie( - @Path("movie_id") id: Long, + @Path("movie_id") id: Int, @Query("api_key") apiKey: String, @Query("language") language: String ): Movie diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/usecase/DelayUseCase.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/usecase/DelayUseCase.kt new file mode 100644 index 000000000..d22b9a795 --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/usecase/DelayUseCase.kt @@ -0,0 +1,34 @@ +package org.michaelbel.movies.domain.usecase + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import org.michaelbel.movies.common.coroutines.Dispatcher +import org.michaelbel.movies.common.coroutines.MoviesDispatchers +import org.michaelbel.movies.common.usecase.UseCase +import org.michaelbel.movies.domain.preferences.constants.PREFERENCE_NETWORK_REQUEST_DELAY +import javax.inject.Inject + +class DelayUseCase @Inject constructor( + @Dispatcher(MoviesDispatchers.IO) private val dispatcher: CoroutineDispatcher, + private val dataStore: DataStore +): UseCase(dispatcher) { + + val networkRequestDelay: Flow = dataStore.data.map { preferences -> + preferences[PREFERENCE_NETWORK_REQUEST_DELAY] ?: 0 + } + + suspend fun networkRequestDelay(): Long { + return dataStore.data.first()[PREFERENCE_NETWORK_REQUEST_DELAY]?.toLong() ?: 0L + } + + override suspend fun execute(parameters: Int) { + dataStore.edit { preferences -> + preferences[PREFERENCE_NETWORK_REQUEST_DELAY] = parameters + } + } +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/usecase/MovieDetailsCase.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/usecase/MovieDetailsCase.kt index a1157110f..16a93bacc 100644 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/usecase/MovieDetailsCase.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/usecase/MovieDetailsCase.kt @@ -1,14 +1,14 @@ package org.michaelbel.movies.domain.usecase import javax.inject.Inject -import org.michaelbel.movies.domain.interactor.MovieInteractor +import org.michaelbel.movies.domain.interactor.movie.MovieInteractor import org.michaelbel.movies.entities.handle import org.michaelbel.movies.entities.lce.ScreenState class MovieDetailsCase @Inject constructor( private val movieInteractor: MovieInteractor ) { - suspend operator fun invoke(movieId: Long): ScreenState { + suspend operator fun invoke(movieId: Int): ScreenState { movieInteractor.movieDetails(movieId).handle( success = { movieDetailsData -> return ScreenState.Content(movieDetailsData) diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/usecase/SelectThemeCase.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/usecase/SelectThemeCase.kt index 1aaee6fbc..bd7932b68 100644 --- a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/usecase/SelectThemeCase.kt +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/usecase/SelectThemeCase.kt @@ -1,7 +1,7 @@ package org.michaelbel.movies.domain.usecase import javax.inject.Inject -import org.michaelbel.movies.domain.interactor.SettingsInteractor +import org.michaelbel.movies.domain.interactor.settings.SettingsInteractor import org.michaelbel.movies.ui.theme.model.AppTheme class SelectThemeCase @Inject constructor( diff --git a/core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/MoviesDatabaseWorker.kt b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/MoviesDatabaseWorker.kt new file mode 100644 index 000000000..727c4aebe --- /dev/null +++ b/core/domain/src/main/kotlin/org/michaelbel/movies/domain/workers/MoviesDatabaseWorker.kt @@ -0,0 +1,57 @@ +package org.michaelbel.movies.domain.workers + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import org.michaelbel.movies.common.coroutines.Dispatcher +import org.michaelbel.movies.common.coroutines.MoviesDispatchers +import org.michaelbel.movies.domain.data.dao.MovieDao +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.domain.ktx.mapToMovieDb +import org.michaelbel.movies.network.model.MovieResponse + +@HiltWorker +class MoviesDatabaseWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted workerParams: WorkerParameters, + @Dispatcher(MoviesDispatchers.IO) private val dispatcher: CoroutineDispatcher, + private val movieDao: MovieDao +): CoroutineWorker(context, workerParams) { + + override suspend fun doWork(): Result { + return withContext(dispatcher) { + try { + val filename: String? = inputData.getString(KEY_FILENAME) + if (filename != null) { + applicationContext.assets.open(filename).use { inputStream -> + val format = Json { ignoreUnknownKeys = true } + val moviesJsonData: List = format.decodeFromStream(inputStream) + val moviesDb: List = moviesJsonData.mapIndexed { index, movieResponse -> + movieResponse.mapToMovieDb( + movieList = MovieDb.MOVIES_LOCAL_LIST, + position = index.plus(1) + ) + } + movieDao.insertAllMovies(moviesDb) + Result.success() + } + } else { + Result.failure() + } + } catch (e: Exception) { + Result.failure() + } + } + } + + companion object { + const val KEY_FILENAME = "MOVIES_DATA_FILENAME" + } +} \ No newline at end of file diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/MovieData.kt b/core/entities/src/main/kotlin/org/michaelbel/movies/entities/MovieData.kt deleted file mode 100644 index e1996c036..000000000 --- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/MovieData.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.michaelbel.movies.entities - -data class MovieData( - val id: Int, - val overview: String, - val posterPath: String, - val backdropPath: String, - val releaseDate: String, - val title: String, - val voteAverage: Float, - val genreIds: List -) { - companion object { - const val NOW_PLAYING = "now_playing" - const val DEFAULT_PAGE_SIZE = 10 - - val Empty: MovieData = MovieData( - id = 0, - overview = "", - posterPath = "", - backdropPath = "", - releaseDate = "", - title = "", - voteAverage = 0F, - genreIds = listOf() - ) - } -} \ No newline at end of file diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/MovieDetailsData.kt b/core/entities/src/main/kotlin/org/michaelbel/movies/entities/MovieDetailsData.kt deleted file mode 100644 index 18d7f9f01..000000000 --- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/MovieDetailsData.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.michaelbel.movies.entities - -data class MovieDetailsData( - val id: Int, - val overview: String, - val posterPath: String, - val backdropPath: String, - val releaseDate: String, - val title: String, - val voteAverage: Float -) { - companion object { - val Empty: MovieDetailsData = MovieDetailsData( - id = 0, - overview = "", - posterPath = "", - backdropPath = "", - releaseDate = "", - title = "", - voteAverage = 0F - ) - } -} \ No newline at end of file diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/TmdbConfig.kt b/core/entities/src/main/kotlin/org/michaelbel/movies/entities/TmdbConfig.kt index 36adf292b..4b97d2c12 100644 --- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/TmdbConfig.kt +++ b/core/entities/src/main/kotlin/org/michaelbel/movies/entities/TmdbConfig.kt @@ -1,4 +1,9 @@ package org.michaelbel.movies.entities +const val TMDB_MOVIE_URL = "https://themoviedb.org/movie/%d" + val tmdbApiKey: String - get() = BuildConfig.TMDB_API_KEY \ No newline at end of file + get() = BuildConfig.TMDB_API_KEY + +val isTmdbApiKeyEmpty: Boolean + get() = tmdbApiKey.isEmpty() || tmdbApiKey == "null" \ No newline at end of file diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/ktx/MovieDetailsDataKtx.kt b/core/entities/src/main/kotlin/org/michaelbel/movies/entities/ktx/MovieDetailsDataKtx.kt deleted file mode 100644 index 03793f5ef..000000000 --- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/ktx/MovieDetailsDataKtx.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.michaelbel.movies.entities.ktx - -import org.michaelbel.movies.entities.MovieDetailsData - -val MovieDetailsData.isNotEmpty: Boolean - get() = this != MovieDetailsData.Empty \ No newline at end of file diff --git a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/mapper/MovieMapper.kt b/core/entities/src/main/kotlin/org/michaelbel/movies/entities/mapper/MovieMapper.kt deleted file mode 100644 index a8f77e100..000000000 --- a/core/entities/src/main/kotlin/org/michaelbel/movies/entities/mapper/MovieMapper.kt +++ /dev/null @@ -1,64 +0,0 @@ -package org.michaelbel.movies.entities.mapper - -import java.util.Locale -import javax.inject.Inject -import org.michaelbel.movies.entities.MovieData -import org.michaelbel.movies.entities.MovieDetailsData -import org.michaelbel.movies.network.model.Movie -import org.michaelbel.movies.network.model.MovieResponse - -class MovieMapper @Inject constructor() { - - private val feedFileSize: String by lazy { - String.format(Locale.ENGLISH, TMDB_IMAGE_FILE_SIZE_W500, TMDB_IMAGE_FILE_SIZE_W500) - } - - fun mapToMovieDataList(response: List): List { - return response.map { movieResponse: MovieResponse -> movieResponse.toMovieData() } - } - - fun mapToMovieDetailsData(response: Movie): MovieDetailsData { - return response.toMovieDetailsData() - } - - private fun formatImageUrl(path: String, size: String): String { - return if (path.isNotEmpty()) { - val imageSize: String = size.ifEmpty { TMDB_IMAGE_FILE_SIZE_ORIGINAL } - String.format(Locale.ENGLISH, TMDB_IMAGE_BASE_URL, imageSize, path) - } else { - IMAGE_EMPTY_URL - } - } - - private fun MovieResponse.toMovieData(): MovieData { - return MovieData( - id = id, - overview = overview.orEmpty(), - posterPath = posterPath, - backdropPath = formatImageUrl(backdropPath.orEmpty(), feedFileSize), - releaseDate = releaseDate, - title = title, - voteAverage = voteAverage, - genreIds = genreIds - ) - } - - private fun Movie.toMovieDetailsData(): MovieDetailsData { - return MovieDetailsData( - id = id, - overview = overview.orEmpty(), - posterPath = posterPath.orEmpty(), - backdropPath = formatImageUrl(backdropPath.orEmpty(), feedFileSize), - releaseDate = releaseDate.orEmpty(), - title = title.orEmpty(), - voteAverage = voteAverage - ) - } - - private companion object { - private const val TMDB_IMAGE_BASE_URL = "https://image.tmdb.org/t/p/%s/%s" - private const val TMDB_IMAGE_FILE_SIZE_ORIGINAL = "original" - private const val TMDB_IMAGE_FILE_SIZE_W500 = "w500" - private const val IMAGE_EMPTY_URL = "https://null" - } -} \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 3cbf46687..b05fb442e 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -36,7 +36,7 @@ android { } dependencies { - implementation(libs.kotlin.serialization) + api(libs.kotlin.serialization) implementation(libs.okhttp.logging.interceptor) implementation(libs.retrofit.converter.serialization) api(libs.retrofit) diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/ktx/ResultKtx.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktx/ResultKtx.kt new file mode 100644 index 000000000..a8f388fc0 --- /dev/null +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktx/ResultKtx.kt @@ -0,0 +1,9 @@ +package org.michaelbel.movies.network.ktx + +import org.michaelbel.movies.network.model.Result + +val Result.nextPage: Int + get() = page.plus(1) + +val Result.isPaginationReached: Boolean + get() = page == totalPages \ No newline at end of file diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/model/Movie.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/Movie.kt index 2c7ad4881..1a2dbbe54 100644 --- a/core/network/src/main/kotlin/org/michaelbel/movies/network/model/Movie.kt +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/model/Movie.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class Movie( - @SerialName("id") val id: Int = 0, + @SerialName("id") val id: Int, @SerialName("imdb_id") val imdbId: String?, @SerialName("adult") val adult: Boolean?, @SerialName("backdrop_path") val backdropPath: String?, 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 4bf4b3d29..f1d1a9332 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 @@ -7,10 +7,15 @@ import kotlinx.serialization.Serializable data class MovieResponse( @SerialName("id") val id: Int, @SerialName("overview") val overview: String?, - @SerialName("poster_path") val posterPath: String, + @SerialName("poster_path") val posterPath: String?, @SerialName("backdrop_path") val backdropPath: String?, @SerialName("release_date") val releaseDate: String, @SerialName("title") val title: String, @SerialName("vote_average") val voteAverage: Float, @SerialName("genre_ids") val genreIds: List -) \ No newline at end of file +) { + companion object { + const val NOW_PLAYING = "now_playing" + const val DEFAULT_PAGE_SIZE = 20 + } +} \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 12558c740..cf2031a55 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -24,7 +24,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.kotlin.compiler.extension.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } lint { diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icon/MoviesIcons.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icon/MoviesIcons.kt index 4f42e28cc..4b0f1abac 100644 --- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icon/MoviesIcons.kt +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/icon/MoviesIcons.kt @@ -4,6 +4,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.Share import androidx.compose.ui.graphics.vector.ImageVector import org.michaelbel.movies.ui.R @@ -18,4 +19,5 @@ object MoviesIcons { val ArrowBack = Icons.Filled.ArrowBack val Info = Icons.Filled.Info val Settings = Icons.Outlined.Settings + val Share = Icons.Outlined.Share } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/language/preview/LanguagesPreviewParameterProvider.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/language/preview/LanguagePreviewParameterProvider.kt similarity index 78% rename from core/ui/src/main/kotlin/org/michaelbel/movies/ui/language/preview/LanguagesPreviewParameterProvider.kt rename to core/ui/src/main/kotlin/org/michaelbel/movies/ui/language/preview/LanguagePreviewParameterProvider.kt index 31fa46997..0f35d776f 100644 --- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/language/preview/LanguagesPreviewParameterProvider.kt +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/language/preview/LanguagePreviewParameterProvider.kt @@ -3,7 +3,7 @@ package org.michaelbel.movies.ui.language.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import org.michaelbel.movies.ui.language.model.AppLanguage -class LanguagesPreviewParameterProvider: PreviewParameterProvider { +class LanguagePreviewParameterProvider: PreviewParameterProvider { override val values: Sequence = sequenceOf( AppLanguage.English, AppLanguage.Russian diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/preview/ThemesPreviewParameterProvider.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/preview/ThemePreviewParameterProvider.kt similarity index 80% rename from core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/preview/ThemesPreviewParameterProvider.kt rename to core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/preview/ThemePreviewParameterProvider.kt index d88ee8832..d3e99300c 100644 --- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/preview/ThemesPreviewParameterProvider.kt +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/theme/preview/ThemePreviewParameterProvider.kt @@ -3,7 +3,7 @@ package org.michaelbel.movies.ui.theme.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import org.michaelbel.movies.ui.theme.model.AppTheme -class ThemesPreviewParameterProvider: PreviewParameterProvider { +class ThemePreviewParameterProvider: PreviewParameterProvider { override val values: Sequence = sequenceOf( AppTheme.FollowSystem, AppTheme.NightNo, diff --git a/feature/details-impl/build.gradle.kts b/feature/details-impl/build.gradle.kts index c1563391f..b20423987 100644 --- a/feature/details-impl/build.gradle.kts +++ b/feature/details-impl/build.gradle.kts @@ -33,7 +33,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.kotlin.compiler.extension.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } lint { 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 b9972c24a..68240bd8a 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 @@ -8,8 +8,8 @@ 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.details.ktx.require import org.michaelbel.movies.domain.usecase.MovieDetailsCase import org.michaelbel.movies.entities.lce.ScreenState import org.michaelbel.movies.network.connectivity.NetworkManager @@ -23,7 +23,7 @@ class DetailsViewModel @Inject constructor( networkManager: NetworkManager ): BaseViewModel() { - private val movieId: Long = savedStateHandle.require("movieId") + private val movieId: Int = savedStateHandle.require("movieId") private val _detailsState: MutableStateFlow = MutableStateFlow(ScreenState.Loading) val detailsState: StateFlow = _detailsState.asStateFlow() diff --git a/core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/SavedStateHandleKtx.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ktx/SavedStateHandleKtx.kt similarity index 74% rename from core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/SavedStateHandleKtx.kt rename to feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ktx/SavedStateHandleKtx.kt index 89a92b25e..04aba8b87 100644 --- a/core/common/src/main/kotlin/org/michaelbel/movies/common/ktx/SavedStateHandleKtx.kt +++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ktx/SavedStateHandleKtx.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.common.ktx +package org.michaelbel.movies.details.ktx import androidx.lifecycle.SavedStateHandle 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 63e4068e8..494cb5d61 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,15 +3,19 @@ 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.MovieDetailsData +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.domain.data.ktx.url import org.michaelbel.movies.entities.lce.ScreenState -internal val ScreenState.Content<*>.movie: MovieDetailsData - get() = data as MovieDetailsData +internal val ScreenState.Content<*>.movie: MovieDb + get() = data as MovieDb internal val ScreenState.toolbarTitle: String @Composable get() = when (this) { is ScreenState.Loading -> stringResource(R.string.details_title) is ScreenState.Content<*> -> movie.title is ScreenState.Failure -> stringResource(R.string.details_title) - } \ No newline at end of file + } + +internal val ScreenState.movieUrl: String? + get() = if (this is ScreenState.Content<*>) movie.url else null \ No newline at end of file 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 1683377ff..03affb277 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 @@ -32,15 +32,15 @@ import com.google.accompanist.placeholder.material.fade import com.google.accompanist.placeholder.placeholder import org.michaelbel.movies.details.ui.preview.MoviePreviewParameterProvider import org.michaelbel.movies.details_impl.R -import org.michaelbel.movies.entities.MovieDetailsData -import org.michaelbel.movies.entities.ktx.isNotEmpty +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.domain.data.ktx.isNotEmpty import org.michaelbel.movies.ui.ktx.isErrorOrEmpty import org.michaelbel.movies.ui.preview.DevicePreviews import org.michaelbel.movies.ui.theme.MoviesTheme @Composable internal fun DetailsContent( - movie: MovieDetailsData, + movie: MovieDb, modifier: Modifier = Modifier, placeholder: Boolean = false ) { @@ -195,7 +195,7 @@ internal fun DetailsContent( @Composable @DevicePreviews private fun DetailsContentPreview( - @PreviewParameter(MoviePreviewParameterProvider::class) movie: MovieDetailsData + @PreviewParameter(MoviePreviewParameterProvider::class) movie: MovieDb ) { MoviesTheme { DetailsContent( diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsLoading.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsLoading.kt index fd0245bf2..4963df8a6 100644 --- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsLoading.kt +++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsLoading.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import org.michaelbel.movies.entities.MovieDetailsData +import org.michaelbel.movies.domain.data.entity.MovieDb import org.michaelbel.movies.ui.preview.DevicePreviews import org.michaelbel.movies.ui.theme.MoviesTheme @@ -15,7 +15,7 @@ internal fun DetailsLoading( ) { DetailsContent( modifier = modifier, - movie = MovieDetailsData.Empty, + movie = MovieDb.Empty, placeholder = true ) } 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 29e126260..e3c187556 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 @@ -13,6 +13,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle 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 @@ -57,7 +58,8 @@ internal fun DetailsScreenContent( modifier = Modifier .statusBarsPadding(), onNavigationIconClick = onBackClick, - movieTitle = detailsState.toolbarTitle + movieTitle = detailsState.toolbarTitle, + movieUrl = detailsState.movieUrl ) }, containerColor = MaterialTheme.colorScheme.primaryContainer 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 29f394a60..4c41a2f64 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 @@ -1,7 +1,9 @@ package org.michaelbel.movies.details.ui +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -10,6 +12,7 @@ import androidx.compose.material3.TopAppBarDefaults 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.details.ui.preview.TitlePreviewParameterProvider @@ -21,27 +24,42 @@ import org.michaelbel.movies.ui.theme.MoviesTheme internal fun DetailsToolbar( modifier: Modifier = Modifier, onNavigationIconClick: () -> Unit, - movieTitle: String + movieTitle: String, + movieUrl: String? ) { TopAppBar( title = { Text( text = movieTitle, + color = MaterialTheme.colorScheme.onPrimaryContainer, overflow = TextOverflow.Ellipsis, maxLines = 1, style = MaterialTheme.typography.titleLarge ) }, modifier = modifier, + actions = { + AnimatedVisibility( + visible = movieUrl != null, + enter = fadeIn() + ) { + if (movieUrl != null) { + ShareButton( + movieUrl = movieUrl + ) + } + } + }, navigationIcon = { IconButton( onClick = { onNavigationIconClick() } ) { - Icon( + Image( imageVector = MoviesIcons.ArrowBack, - contentDescription = null + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer) ) } }, @@ -61,7 +79,8 @@ private fun DetailsToolbarPreview( modifier = Modifier .statusBarsPadding(), onNavigationIconClick = {}, - movieTitle = title + movieTitle = title, + movieUrl = null ) } } \ No newline at end of file diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/ShareButton.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/ShareButton.kt new file mode 100644 index 000000000..66cf54e91 --- /dev/null +++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/ShareButton.kt @@ -0,0 +1,62 @@ +package org.michaelbel.movies.details.ui + +import android.content.Context +import android.content.Intent +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.PreviewParameter +import org.michaelbel.movies.details.ui.preview.MoviePreviewParameterProvider +import org.michaelbel.movies.details_impl.R +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.domain.data.ktx.url +import org.michaelbel.movies.ui.icon.MoviesIcons +import org.michaelbel.movies.ui.preview.DevicePreviews +import org.michaelbel.movies.ui.theme.MoviesTheme + +@Composable +internal fun ShareButton( + movieUrl: String +) { + val context: Context = LocalContext.current + val resultContract = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) {} + + IconButton( + onClick = { + Intent().apply { + type = "text/plain" + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, movieUrl) + }.also { intent: Intent -> + resultContract.launch( + Intent.createChooser(intent, context.getString(R.string.details_share_via)) + ) + } + } + ) { + Image( + imageVector = MoviesIcons.Share, + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer) + ) + } +} + +@Composable +@DevicePreviews +private fun ShareButtonPreview( + @PreviewParameter(MoviePreviewParameterProvider::class) movie: MovieDb +) { + MoviesTheme { + ShareButton( + movieUrl = movie.url + ) + } +} \ No newline at end of file diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/preview/MoviePreviewParameterProvider.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/preview/MoviePreviewParameterProvider.kt index 2a360f62d..a092dbaa8 100644 --- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/preview/MoviePreviewParameterProvider.kt +++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/preview/MoviePreviewParameterProvider.kt @@ -1,20 +1,26 @@ package org.michaelbel.movies.details.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import org.michaelbel.movies.entities.MovieDetailsData +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.network.model.Movie -internal class MoviePreviewParameterProvider: PreviewParameterProvider { - override val values: Sequence = sequenceOf( - MovieDetailsData( - id = 438148, - overview = """Миллион лет миньоны искали самого великого и ужасного предводителя, - пока не встретили ЕГО. Знакомьтесь - Грю. Пусть он еще очень молод, но у него - в планах по-настоящему гадкие дела, которые заставят планету содрогнуться.""", - posterPath = "/19GXuePqcZSPD5JgT9MeVdeu9Tc.jpg", - backdropPath = "https://image.tmdb.org/t/p/w500//nmGWzTLMXy9x7mKd8NKPLmHtWGa.jpg", - releaseDate = "2022-06-29", - title = "Миньоны: Грювитация", - voteAverage = 7.6F +internal class MoviePreviewParameterProvider: PreviewParameterProvider { + override val values: Sequence = sequenceOf( + MovieDb( + movieList = Movie.NOW_PLAYING, + dateAdded = System.currentTimeMillis(), + position = 0, + movieId = 438148, + overview = """Queen Ramonda, Shuri, M’Baku, Okoye and the Dora Milaje fight to protect + their nation from intervening world powers in the wake of King T’Challa’s death. + As the Wakandans strive to embrace their next chapter, the heroes must band + together with the help of War Dog Nakia and Everett Ross and forge a new path for + the kingdom of Wakanda.""", + posterPath = "", + backdropPath = "", + releaseDate = "2022-11-09", + title = "Black Panther: Wakanda Forever", + voteAverage = 7.5F ) ) } \ No newline at end of file diff --git a/feature/details-impl/src/main/res/values-ru/strings.xml b/feature/details-impl/src/main/res/values-ru/strings.xml index c7065107d..6f174f370 100644 --- a/feature/details-impl/src/main/res/values-ru/strings.xml +++ b/feature/details-impl/src/main/res/values-ru/strings.xml @@ -3,4 +3,5 @@ Детали фильма Во время загрузки произошла ошибка Картинка не загрузилась + Поделиться через \ No newline at end of file diff --git a/feature/details-impl/src/main/res/values/strings.xml b/feature/details-impl/src/main/res/values/strings.xml index 69d64c6eb..e5e4db767 100644 --- a/feature/details-impl/src/main/res/values/strings.xml +++ b/feature/details-impl/src/main/res/values/strings.xml @@ -3,4 +3,5 @@ Movie Details Error while loading No Image + Share via \ No newline at end of file diff --git a/feature/details/build.gradle.kts b/feature/details/build.gradle.kts index 19a5e0aca..4ba87df58 100644 --- a/feature/details/build.gradle.kts +++ b/feature/details/build.gradle.kts @@ -24,7 +24,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.kotlin.compiler.extension.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } lint { diff --git a/feature/feed-impl/build.gradle.kts b/feature/feed-impl/build.gradle.kts index 2a33c4ae8..67796b7c4 100644 --- a/feature/feed-impl/build.gradle.kts +++ b/feature/feed-impl/build.gradle.kts @@ -23,7 +23,8 @@ android { kotlinOptions { freeCompilerArgs = freeCompilerArgs + listOf( "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", - "-opt-in=androidx.lifecycle.compose.ExperimentalLifecycleComposeApi" + "-opt-in=androidx.lifecycle.compose.ExperimentalLifecycleComposeApi", + "-opt-in=androidx.paging.ExperimentalPagingApi" ) } @@ -32,7 +33,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.kotlin.compiler.extension.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } lint { 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 130c4888d..57ea81c97 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 @@ -5,32 +5,44 @@ 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.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import org.michaelbel.movies.common.viewmodel.BaseViewModel -import org.michaelbel.movies.domain.interactor.MovieInteractor -import org.michaelbel.movies.domain.interactor.SettingsInteractor -import org.michaelbel.movies.entities.MovieData +import org.michaelbel.movies.domain.data.AppDatabase +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.domain.interactor.movie.MovieInteractor +import org.michaelbel.movies.domain.interactor.settings.SettingsInteractor +import org.michaelbel.movies.domain.mediator.MoviesRemoteMediator +import org.michaelbel.movies.entities.isTmdbApiKeyEmpty import org.michaelbel.movies.network.connectivity.NetworkManager import org.michaelbel.movies.network.connectivity.NetworkStatus -import javax.inject.Inject +import org.michaelbel.movies.network.model.MovieResponse @HiltViewModel class FeedViewModel @Inject constructor( private val movieInteractor: MovieInteractor, + appDatabase: AppDatabase, settingsInteractor: SettingsInteractor, networkManager: NetworkManager ): BaseViewModel() { - val pagingItems: Flow> = Pager( - PagingConfig( - pageSize = MovieData.DEFAULT_PAGE_SIZE - ) - ) { - MoviesPagingSource(movieInteractor) - }.flow + private val moviesList: String + get() = if (isTmdbApiKeyEmpty) MovieDb.MOVIES_LOCAL_LIST else MovieResponse.NOW_PLAYING + + val pagingItems: Flow> = Pager( + config = PagingConfig( + pageSize = MovieResponse.DEFAULT_PAGE_SIZE + ), + remoteMediator = MoviesRemoteMediator( + appDatabase = appDatabase, + movieInteractor = movieInteractor, + movieList = MovieResponse.NOW_PLAYING + ), + pagingSourceFactory = { movieInteractor.moviesPagingSource(moviesList) } + ).flow .stateIn( scope = this, started = SharingStarted.Lazily, diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/MoviesPagingSource.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/MoviesPagingSource.kt deleted file mode 100644 index 6bc4dbd0b..000000000 --- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/MoviesPagingSource.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.michaelbel.movies.feed - -import androidx.paging.PagingSource -import androidx.paging.PagingState -import org.michaelbel.movies.domain.interactor.MovieInteractor -import org.michaelbel.movies.entities.MovieData - -internal class MoviesPagingSource( - private val movieInteractor: MovieInteractor -): PagingSource() { - - override fun getRefreshKey(state: PagingState): Int? { - val anchorPosition: Int = state.anchorPosition ?: return null - val page = state.closestPageToPosition(anchorPosition) ?: return null - return page.prevKey?.plus(1) ?: page.nextKey?.minus(1) - } - - override suspend fun load(params: LoadParams): LoadResult { - return try { - val page: Int = params.key ?: 1 - - val (items, totalPages) = movieInteractor.movieList( - list = MovieData.NOW_PLAYING, - page = page - ) - - LoadResult.Page( - data = items, - prevKey = if (page == 1) null else page - 1, - nextKey = if (page == totalPages) null else page + 1 - ) - } catch (e: Exception) { - LoadResult.Error(e) - } - } -} \ No newline at end of file diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ktx/LazyPagingItemsKtx.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ktx/LazyPagingItemsKtx.kt index 18a2a15b0..d70faaaea 100644 --- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ktx/LazyPagingItemsKtx.kt +++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ktx/LazyPagingItemsKtx.kt @@ -3,7 +3,7 @@ package org.michaelbel.movies.feed.ktx import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems -private val LazyPagingItems.isNotEmpty: Boolean +internal val LazyPagingItems.isNotEmpty: Boolean get() = itemCount > 0 internal val LazyPagingItems.isEmpty: Boolean diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedApiKeyBox.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedApiKeyBox.kt new file mode 100644 index 000000000..3f4a97918 --- /dev/null +++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedApiKeyBox.kt @@ -0,0 +1,47 @@ +package org.michaelbel.movies.feed.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.michaelbel.movies.feed_impl.R +import org.michaelbel.movies.ui.preview.DevicePreviews +import org.michaelbel.movies.ui.theme.MoviesTheme + +@Composable +internal fun FeedApiKeyBox( + modifier: Modifier = Modifier +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.feed_error_api_key_null), + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.bodyLarge + ) + } +} + +@Composable +@DevicePreviews +private fun FeedApiKeyBoxPreview() { + MoviesTheme { + FeedApiKeyBox( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.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/FeedContent.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedContent.kt index 058874a36..07603409c 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 @@ -15,7 +15,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.items -import org.michaelbel.movies.entities.MovieData +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.entities.isTmdbApiKeyEmpty +import org.michaelbel.movies.feed.ktx.isNotEmpty import org.michaelbel.movies.feed.ktx.isPagingFailure import org.michaelbel.movies.feed.ktx.isPagingLoading @@ -24,7 +26,7 @@ internal fun FeedContent( modifier: Modifier = Modifier, paddingValues: PaddingValues = PaddingValues(), listState: LazyListState, - pagingItems: LazyPagingItems, + pagingItems: LazyPagingItems, onMovieClick: (Int) -> Unit ) { LazyColumn( @@ -38,20 +40,27 @@ internal fun FeedContent( modifier = Modifier .fillMaxWidth() .padding( - start = 16.dp, - top = 4.dp, - end = 16.dp, - bottom = 4.dp + horizontal = 16.dp, + vertical = 4.dp ) .clip(MaterialTheme.shapes.small) .background(MaterialTheme.colorScheme.inversePrimary) .clickable { - onMovieClick(movie.id) + onMovieClick(movie.movieId) }, movie = movie ) } } + if (isTmdbApiKeyEmpty && pagingItems.isNotEmpty) { + item { + FeedApiKeyBox( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) + } + } pagingItems.apply { when { isPagingLoading -> { 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 index f73ef16da..aea8aa8ac 100644 --- 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 @@ -13,7 +13,8 @@ import androidx.compose.ui.unit.dp import com.google.accompanist.placeholder.PlaceholderHighlight import com.google.accompanist.placeholder.material.fade import com.google.accompanist.placeholder.placeholder -import org.michaelbel.movies.entities.MovieData +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.network.model.MovieResponse import org.michaelbel.movies.ui.preview.DevicePreviews import org.michaelbel.movies.ui.theme.MoviesTheme @@ -26,7 +27,7 @@ internal fun FeedLoading( modifier = modifier, contentPadding = paddingValues ) { - items(MovieData.DEFAULT_PAGE_SIZE) { + items(MovieResponse.DEFAULT_PAGE_SIZE) { FeedMovieBox( modifier = Modifier .fillMaxWidth() @@ -40,7 +41,7 @@ internal fun FeedLoading( shape = MaterialTheme.shapes.small, highlight = PlaceholderHighlight.fade() ), - movie = MovieData.Empty + movie = MovieDb.Empty ) } } diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedMovieBox.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedMovieBox.kt index cd24c75bc..c37971d25 100644 --- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedMovieBox.kt +++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/FeedMovieBox.kt @@ -25,9 +25,9 @@ import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import coil.compose.AsyncImage import coil.request.ImageRequest -import org.michaelbel.movies.entities.MovieData -import org.michaelbel.movies.feed_impl.R +import org.michaelbel.movies.domain.data.entity.MovieDb import org.michaelbel.movies.feed.ui.preview.MoviePreviewParameterProvider +import org.michaelbel.movies.feed_impl.R import org.michaelbel.movies.ui.ktx.isErrorOrEmpty import org.michaelbel.movies.ui.preview.DevicePreviews import org.michaelbel.movies.ui.theme.MoviesTheme @@ -35,7 +35,7 @@ import org.michaelbel.movies.ui.theme.MoviesTheme @Composable internal fun FeedMovieBox( modifier: Modifier = Modifier, - movie: MovieData + movie: MovieDb ) { val context: Context = LocalContext.current var isNoImageVisible: Boolean by remember { mutableStateOf(false) } @@ -107,17 +107,15 @@ internal fun FeedMovieBox( @Composable @DevicePreviews private fun MovieBoxPreview( - @PreviewParameter(MoviePreviewParameterProvider::class) movie: MovieData + @PreviewParameter(MoviePreviewParameterProvider::class) movie: MovieDb ) { MoviesTheme { FeedMovieBox( modifier = Modifier .fillMaxWidth() .padding( - start = 16.dp, - top = 4.dp, - end = 16.dp, - bottom = 4.dp + horizontal = 16.dp, + vertical = 4.dp ) .clip(MaterialTheme.shapes.small) .background(MaterialTheme.colorScheme.inversePrimary), 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 f3eb29b71..5a8a79578 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 @@ -30,8 +30,8 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import org.michaelbel.movies.domain.data.entity.MovieDb import org.michaelbel.movies.domain.exceptions.ApiKeyNotNullException -import org.michaelbel.movies.entities.MovieData import org.michaelbel.movies.feed.FeedViewModel import org.michaelbel.movies.feed.ktx.isFailure import org.michaelbel.movies.feed.ktx.isLoading @@ -48,7 +48,7 @@ fun FeedRoute( modifier: Modifier = Modifier, viewModel: FeedViewModel = hiltViewModel() ) { - val pagingItems: LazyPagingItems = viewModel.pagingItems.collectAsLazyPagingItems() + val pagingItems: LazyPagingItems = viewModel.pagingItems.collectAsLazyPagingItems() val isSettingsIconVisible: Boolean by viewModel.isSettingsIconVisible.collectAsStateWithLifecycle() val networkStatus: NetworkStatus by viewModel.networkStatus.collectAsStateWithLifecycle() @@ -67,7 +67,7 @@ internal fun FeedScreenContent( onNavigateToSettings: () -> Unit, onNavigateToDetails: (Int) -> Unit, modifier: Modifier = Modifier, - pagingItems: LazyPagingItems, + pagingItems: LazyPagingItems, networkStatus: NetworkStatus, isSettingsIconVisible: Boolean ) { @@ -114,7 +114,7 @@ internal fun FeedScreenContent( .clip(MaterialTheme.shapes.extraLarge) .clickableWithoutRipple { onScrollToTop() }, isSettingsIconVisible = isSettingsIconVisible, - onNavigationIconClick = onNavigateToSettings + onSettingsIconClick = onNavigateToSettings ) }, snackbarHost = { @@ -136,10 +136,13 @@ internal fun FeedScreenContent( FeedFailure( modifier = Modifier .padding(paddingValues) - .fillMaxSize(), + .fillMaxSize() + .clickableWithoutRipple { pagingItems.retry() }, onCheckConnectivityClick = { if (Build.VERSION.SDK_INT >= 29) { - resultContract.launch(Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY)) + resultContract.launch( + Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY) + ) } } ) 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 10d88d5f6..6007d5b37 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 @@ -23,7 +23,7 @@ import org.michaelbel.movies.ui.theme.MoviesTheme internal fun FeedToolbar( modifier: Modifier = Modifier, isSettingsIconVisible: Boolean, - onNavigationIconClick: () -> Unit + onSettingsIconClick: () -> Unit ) { TopAppBar( title = { @@ -39,7 +39,7 @@ internal fun FeedToolbar( actions = { if (isSettingsIconVisible) { IconButton( - onClick = onNavigationIconClick + onClick = onSettingsIconClick ) { Image( imageVector = MoviesIcons.Settings, @@ -65,7 +65,7 @@ private fun FeedToolbarPreview( modifier = Modifier .statusBarsPadding(), isSettingsIconVisible = visible, - onNavigationIconClick = {} + onSettingsIconClick = {} ) } } \ No newline at end of file diff --git a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/preview/MoviePreviewParameterProvider.kt b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/preview/MoviePreviewParameterProvider.kt index b7147c1be..fce430a2e 100644 --- a/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/preview/MoviePreviewParameterProvider.kt +++ b/feature/feed-impl/src/main/kotlin/org/michaelbel/movies/feed/ui/preview/MoviePreviewParameterProvider.kt @@ -1,19 +1,26 @@ package org.michaelbel.movies.feed.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import org.michaelbel.movies.entities.MovieData +import org.michaelbel.movies.domain.data.entity.MovieDb +import org.michaelbel.movies.network.model.Movie -internal class MoviePreviewParameterProvider: PreviewParameterProvider { - override val values: Sequence = sequenceOf( - MovieData( - id = 438148, - overview = "", - posterPath = "/19GXuePqcZSPD5JgT9MeVdeu9Tc.jpg", - backdropPath = "https://image.tmdb.org/t/p/w500//nmGWzTLMXy9x7mKd8NKPLmHtWGa.jpg", - releaseDate = "2022-06-29", - title = "Миньоны: Грювитация", - voteAverage = 7.6F, - genreIds = listOf(16, 12, 35, 14) +internal class MoviePreviewParameterProvider: PreviewParameterProvider { + override val values: Sequence = sequenceOf( + MovieDb( + movieList = Movie.NOW_PLAYING, + dateAdded = System.currentTimeMillis(), + position = 0, + movieId = 438148, + overview = """Queen Ramonda, Shuri, M’Baku, Okoye and the Dora Milaje fight to protect + their nation from intervening world powers in the wake of King T’Challa’s death. + As the Wakandans strive to embrace their next chapter, the heroes must band + together with the help of War Dog Nakia and Everett Ross and forge a new path for + the kingdom of Wakanda.""", + posterPath = "", + backdropPath = "", + releaseDate = "2022-11-09", + title = "Black Panther: Wakanda Forever", + voteAverage = 7.5F ) ) } \ No newline at end of file diff --git a/feature/feed/build.gradle.kts b/feature/feed/build.gradle.kts index 45dc161b6..a641d7fdc 100644 --- a/feature/feed/build.gradle.kts +++ b/feature/feed/build.gradle.kts @@ -24,7 +24,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.kotlin.compiler.extension.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } lint { diff --git a/feature/settings-impl/build.gradle.kts b/feature/settings-impl/build.gradle.kts index ba50e383e..fba431793 100644 --- a/feature/settings-impl/build.gradle.kts +++ b/feature/settings-impl/build.gradle.kts @@ -34,7 +34,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.kotlin.compiler.extension.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } lint { 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 f32f3d8ea..573adf49b 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 @@ -3,16 +3,14 @@ package org.michaelbel.movies.settings import android.os.Build import androidx.compose.ui.unit.LayoutDirection import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner import dagger.hilt.android.lifecycle.HiltViewModel -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.viewmodel.BaseViewModel -import org.michaelbel.movies.domain.interactor.SettingsInteractor +import org.michaelbel.movies.domain.interactor.settings.SettingsInteractor +import org.michaelbel.movies.domain.usecase.DelayUseCase import org.michaelbel.movies.domain.usecase.SelectLanguageCase import org.michaelbel.movies.domain.usecase.SelectThemeCase import org.michaelbel.movies.ui.language.model.AppLanguage @@ -24,7 +22,8 @@ import javax.inject.Inject class SettingsViewModel @Inject constructor( private val settingsInteractor: SettingsInteractor, private val selectLanguageCase: SelectLanguageCase, - private val selectThemeCase: SelectThemeCase + private val selectThemeCase: SelectThemeCase, + private val delayUseCase: DelayUseCase ): BaseViewModel(), DefaultLifecycleObserver { val isDynamicColorsFeatureEnabled: Boolean = Build.VERSION.SDK_INT >= 31 @@ -77,7 +76,7 @@ class SettingsViewModel @Inject constructor( initialValue = false ) - val networkRequestDelay: StateFlow = settingsInteractor.networkRequestDelay + val networkRequestDelay: StateFlow = delayUseCase.networkRequestDelay .stateIn( scope = this, started = SharingStarted.Lazily, @@ -91,18 +90,6 @@ class SettingsViewModel @Inject constructor( initialValue = AppVersionData.None ) - private val _areNotificationsEnabled: MutableStateFlow = MutableStateFlow(false) - val areNotificationsEnabled: StateFlow = _areNotificationsEnabled.asStateFlow() - - init { - checkNotificationsEnabled() - } - - override fun onResume(owner: LifecycleOwner) { - super.onResume(owner) - checkNotificationsEnabled() - } - fun selectLanguage(language: AppLanguage) = launch { selectLanguageCase(language) } @@ -120,10 +107,6 @@ class SettingsViewModel @Inject constructor( } fun setNetworkRequestDelay(value: Int) = launch { - settingsInteractor.setNetworkRequestDelay(value) - } - - fun checkNotificationsEnabled() { - _areNotificationsEnabled.value = settingsInteractor.areNotificationsEnabled + delayUseCase(value) } } \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/AppLanguageKtx.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/AppLanguageKtx.kt index aa89ec494..2d87f2919 100644 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/AppLanguageKtx.kt +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/AppLanguageKtx.kt @@ -1,10 +1,12 @@ package org.michaelbel.movies.settings.ktx +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import org.michaelbel.movies.settings_impl.R import org.michaelbel.movies.ui.language.model.AppLanguage -internal val AppLanguage.languageTextRes: Int - get() = when (this) { - is AppLanguage.English -> R.string.settings_language_en - is AppLanguage.Russian -> R.string.settings_language_ru +internal val AppLanguage.languageText: String + @Composable get() = when (this) { + is AppLanguage.English -> stringResource(R.string.settings_language_en) + is AppLanguage.Russian -> stringResource(R.string.settings_language_ru) } \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/NotificationManagerKtx.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/NotificationManagerKtx.kt new file mode 100644 index 000000000..4cf9661fe --- /dev/null +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/NotificationManagerKtx.kt @@ -0,0 +1,8 @@ +package org.michaelbel.movies.settings.ktx + +import android.app.NotificationManager +import android.content.Context +import androidx.core.content.ContextCompat + +internal val Context.notificationManager: NotificationManager + get() = ContextCompat.getSystemService(this, NotificationManager::class.java) as NotificationManager \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SystemThemeKtx.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SystemThemeKtx.kt index dfc8ca9d0..86770ecb4 100644 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SystemThemeKtx.kt +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ktx/SystemThemeKtx.kt @@ -1,11 +1,13 @@ package org.michaelbel.movies.settings.ktx +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import org.michaelbel.movies.settings_impl.R import org.michaelbel.movies.ui.theme.model.AppTheme -internal val AppTheme.themeTextRes: Int - get() = when (this) { - is AppTheme.NightNo -> R.string.settings_theme_light - is AppTheme.NightYes -> R.string.settings_theme_dark - is AppTheme.FollowSystem -> R.string.settings_theme_system +internal val AppTheme.themeText: String + @Composable get() = when (this) { + is AppTheme.NightNo -> stringResource(R.string.settings_theme_light) + is AppTheme.NightYes -> stringResource(R.string.settings_theme_dark) + is AppTheme.FollowSystem -> stringResource(R.string.settings_theme_system) } \ No newline at end of file diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageBox.kt index 1296bbdf8..4530f0177 100644 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageBox.kt +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsLanguageBox.kt @@ -1,11 +1,16 @@ package org.michaelbel.movies.settings.ui import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -13,20 +18,38 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension +import org.michaelbel.movies.settings.ktx.languageText import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.settings.ktx.languageTextRes import org.michaelbel.movies.ui.language.model.AppLanguage -import org.michaelbel.movies.ui.language.preview.LanguagesPreviewParameterProvider +import org.michaelbel.movies.ui.language.preview.LanguagePreviewParameterProvider import org.michaelbel.movies.ui.preview.DevicePreviews import org.michaelbel.movies.ui.theme.MoviesTheme @Composable fun SettingsLanguageBox( modifier: Modifier = Modifier, - currentLanguage: AppLanguage + languages: List, + currentLanguage: AppLanguage, + onLanguageSelect: (AppLanguage) -> Unit ) { + var languageDialog: Boolean by remember { mutableStateOf(false) } + + if (languageDialog) { + SettingLanguageDialog( + languages = languages, + currentLanguage = currentLanguage, + onLanguageSelect = onLanguageSelect, + onDismissRequest = { + languageDialog = false + } + ) + } + ConstraintLayout( modifier = modifier + .clickable { + languageDialog = true + } .testTag("ConstraintLayout") ) { val (title, value) = createRefs() @@ -47,7 +70,7 @@ fun SettingsLanguageBox( ) Text( - text = stringResource(currentLanguage.languageTextRes), + text = currentLanguage.languageText, modifier = Modifier .constrainAs(value) { width = Dimension.wrapContent @@ -66,7 +89,7 @@ fun SettingsLanguageBox( @Composable @DevicePreviews private fun SettingsLanguageBoxPreview( - @PreviewParameter(LanguagesPreviewParameterProvider::class) language: AppLanguage + @PreviewParameter(LanguagePreviewParameterProvider::class) language: AppLanguage ) { MoviesTheme { SettingsLanguageBox( @@ -74,7 +97,9 @@ private fun SettingsLanguageBoxPreview( .fillMaxWidth() .height(48.dp) .background(MaterialTheme.colorScheme.primaryContainer), - currentLanguage = language + languages = listOf(AppLanguage.English, AppLanguage.Russian), + currentLanguage = language, + onLanguageSelect = {} ) } } \ No newline at end of file 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 96928df2f..e2f48ba94 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 @@ -22,11 +22,11 @@ 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.settings.ktx.languageText import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.settings.ktx.languageTextRes import org.michaelbel.movies.ui.icon.MoviesIcons import org.michaelbel.movies.ui.language.model.AppLanguage -import org.michaelbel.movies.ui.language.preview.LanguagesPreviewParameterProvider +import org.michaelbel.movies.ui.language.preview.LanguagePreviewParameterProvider import org.michaelbel.movies.ui.preview.DevicePreviews import org.michaelbel.movies.ui.theme.MoviesTheme @@ -115,7 +115,7 @@ private fun SettingLanguageDialogContent( ) Text( - text = stringResource(language.languageTextRes), + text = language.languageText, modifier = Modifier .padding( start = 8.dp @@ -131,7 +131,7 @@ private fun SettingLanguageDialogContent( @Composable @DevicePreviews private fun SettingLanguageDialogPreview( - @PreviewParameter(LanguagesPreviewParameterProvider::class) language: AppLanguage + @PreviewParameter(LanguagePreviewParameterProvider::class) language: AppLanguage ) { MoviesTheme { SettingLanguageDialog( 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 ad8a48e30..bdd4e5777 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 @@ -1,31 +1,92 @@ package org.michaelbel.movies.settings.ui +import android.Manifest +import android.app.Activity +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.provider.Settings +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.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext 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.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension +import androidx.core.net.toUri +import org.michaelbel.movies.settings.ktx.notificationManager +import org.michaelbel.movies.settings.ui.lifecycle.OnResume import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.ui.preview.BooleanPreviewParameterProvider import org.michaelbel.movies.ui.preview.DevicePreviews import org.michaelbel.movies.ui.theme.MoviesTheme @Composable fun SettingsPostNotificationsBox( modifier: Modifier = Modifier, - areNotificationsEnabled: Boolean + onShowPermissionSnackbar: () -> Unit ) { + val context: Context = LocalContext.current + val notificationManager: NotificationManager = context.notificationManager + var areNotificationsEnabled: Boolean by remember { mutableStateOf(notificationManager.areNotificationsEnabled()) } + + val resultContract = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) {} + + val onStartAppSettingsIntent: () -> Unit = { + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + "package:${context.packageName}".toUri() + ).apply { + addCategory(Intent.CATEGORY_DEFAULT) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }.also { intent: Intent -> + resultContract.launch(intent) + } + } + + val postNotificationsPermission = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) { + areNotificationsEnabled = notificationManager.areNotificationsEnabled() + } else { + val shouldRequest: Boolean = (context as Activity).shouldShowRequestPermissionRationale( + Manifest.permission.POST_NOTIFICATIONS + ) + if (!shouldRequest) { + onShowPermissionSnackbar() + } + } + } + + OnResume { + areNotificationsEnabled = notificationManager.areNotificationsEnabled() + } + ConstraintLayout( modifier = modifier + .clickable { + if (areNotificationsEnabled) { + onStartAppSettingsIntent() + } else { + postNotificationsPermission.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } .testTag("ConstraintLayout") ) { val (title, value) = createRefs() @@ -63,16 +124,14 @@ fun SettingsPostNotificationsBox( @Composable @DevicePreviews -private fun SettingsPostNotificationsBoxPreview( - @PreviewParameter(BooleanPreviewParameterProvider::class) isEnabled: Boolean -) { +private fun SettingsPostNotificationsBoxPreview() { MoviesTheme { SettingsPostNotificationsBox( modifier = Modifier .fillMaxWidth() .height(48.dp) .background(MaterialTheme.colorScheme.primaryContainer), - areNotificationsEnabled = isEnabled + onShowPermissionSnackbar = {} ) } } \ No newline at end of file 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 ba5247d83..110827127 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 @@ -1,6 +1,5 @@ package org.michaelbel.movies.settings.ui -import android.Manifest import android.app.Activity import android.content.Context import android.content.Intent @@ -23,25 +22,20 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult 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.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.android.play.core.review.ReviewInfo import com.google.android.play.core.review.ReviewManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import org.michaelbel.movies.common.ktx.denied import org.michaelbel.movies.common.review.rememberReviewManager import org.michaelbel.movies.common.review.rememberReviewTask import org.michaelbel.movies.settings.SettingsViewModel @@ -64,13 +58,9 @@ fun SettingsRoute( val layoutDirection: LayoutDirection by viewModel.layoutDirection.collectAsStateWithLifecycle() val isPlayServicesAvailable: Boolean by viewModel.isPlayServicesAvailable.collectAsStateWithLifecycle() val isAppFromGooglePlay: Boolean by viewModel.isAppFromGooglePlay.collectAsStateWithLifecycle() - val areNotificationsEnabled: Boolean by viewModel.areNotificationsEnabled.collectAsStateWithLifecycle() val networkRequestDelay: Int by viewModel.networkRequestDelay.collectAsStateWithLifecycle() val appVersionData: AppVersionData by viewModel.appVersionData.collectAsStateWithLifecycle() - val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current - lifecycleOwner.lifecycle.addObserver(viewModel) - SettingsScreenContent( onBackClick = onBackClick, modifier = modifier, @@ -86,8 +76,6 @@ fun SettingsRoute( isRtlEnabled = layoutDirection == LayoutDirection.Rtl, onEnableRtlChanged = viewModel::setRtlEnabled, isPostNotificationsFeatureEnabled = viewModel.isPostNotificationsFeatureEnabled, - areNotificationsEnabled = areNotificationsEnabled, - onNotificationsStatusChanged = viewModel::checkNotificationsEnabled, isPlayServicesAvailable = isPlayServicesAvailable, isAppFromGooglePlay = isAppFromGooglePlay, networkRequestDelay = networkRequestDelay, @@ -112,8 +100,6 @@ internal fun SettingsScreenContent( isRtlEnabled: Boolean, onEnableRtlChanged: (Boolean) -> Unit, isPostNotificationsFeatureEnabled: Boolean, - areNotificationsEnabled: Boolean, - onNotificationsStatusChanged: () -> Unit, isPlayServicesAvailable: Boolean, isAppFromGooglePlay: Boolean, networkRequestDelay: Int, @@ -126,31 +112,6 @@ internal fun SettingsScreenContent( val reviewManager: ReviewManager = rememberReviewManager() val reviewInfo: ReviewInfo? = rememberReviewTask(reviewManager) - var languageDialog: Boolean by remember { mutableStateOf(false) } - var themeDialog: Boolean by remember { mutableStateOf(false) } - - if (languageDialog) { - SettingLanguageDialog( - languages = languages, - currentLanguage = currentLanguage, - onLanguageSelect = onLanguageSelect, - onDismissRequest = { - languageDialog = false - } - ) - } - - if (themeDialog) { - SettingThemeDialog( - themes = themes, - currentTheme = currentTheme, - onThemeSelect = onThemeSelect, - onDismissRequest = { - themeDialog = false - } - ) - } - val resultContract = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult() ) {} @@ -180,21 +141,6 @@ internal fun SettingsScreenContent( } } - val postNotificationsPermission = rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted -> - if (granted) { - onNotificationsStatusChanged() - } else { - val shouldRequest: Boolean = (context as Activity).shouldShowRequestPermissionRationale( - Manifest.permission.POST_NOTIFICATIONS - ) - if (!shouldRequest) { - onShowPermissionSnackbar() - } - } - } - val onShowSnackbar: (String) -> Unit = { message -> scope.launch { snackbarHostState.showSnackbar( @@ -220,12 +166,6 @@ internal fun SettingsScreenContent( } } - fun onLaunchPostNotificationsPermission() { - if (Manifest.permission.POST_NOTIFICATIONS.denied(context)) { - postNotificationsPermission.launch(Manifest.permission.POST_NOTIFICATIONS) - } - } - Scaffold( modifier = modifier, topBar = { @@ -257,21 +197,19 @@ internal fun SettingsScreenContent( SettingsLanguageBox( modifier = Modifier .fillMaxWidth() - .height(48.dp) - .clickable { - languageDialog = true - }, - currentLanguage = currentLanguage + .height(48.dp), + languages = languages, + currentLanguage = currentLanguage, + onLanguageSelect = onLanguageSelect ) SettingsThemeBox( modifier = Modifier .fillMaxWidth() - .height(48.dp) - .clickable { - themeDialog = true - }, - currentTheme = currentTheme + .height(48.dp), + themes = themes, + currentTheme = currentTheme, + onThemeSelect = onThemeSelect ) if (isDynamicColorsFeatureEnabled) { @@ -300,15 +238,8 @@ internal fun SettingsScreenContent( SettingsPostNotificationsBox( modifier = Modifier .fillMaxWidth() - .height(48.dp) - .clickable { - if (areNotificationsEnabled) { - onStartAppSettingsIntent() - } else { - onLaunchPostNotificationsPermission() - } - }, - areNotificationsEnabled = areNotificationsEnabled + .height(48.dp), + onShowPermissionSnackbar = onShowPermissionSnackbar ) } diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeBox.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeBox.kt index 55e96470a..719395c8b 100644 --- a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeBox.kt +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/SettingsThemeBox.kt @@ -1,11 +1,16 @@ package org.michaelbel.movies.settings.ui import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -13,20 +18,38 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension +import org.michaelbel.movies.settings.ktx.themeText import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.settings.ktx.themeTextRes import org.michaelbel.movies.ui.preview.DevicePreviews import org.michaelbel.movies.ui.theme.MoviesTheme import org.michaelbel.movies.ui.theme.model.AppTheme -import org.michaelbel.movies.ui.theme.preview.ThemesPreviewParameterProvider +import org.michaelbel.movies.ui.theme.preview.ThemePreviewParameterProvider @Composable fun SettingsThemeBox( modifier: Modifier = Modifier, - currentTheme: AppTheme + themes: List, + currentTheme: AppTheme, + onThemeSelect: (AppTheme) -> Unit ) { + var themeDialog: Boolean by remember { mutableStateOf(false) } + + if (themeDialog) { + SettingThemeDialog( + themes = themes, + currentTheme = currentTheme, + onThemeSelect = onThemeSelect, + onDismissRequest = { + themeDialog = false + } + ) + } + ConstraintLayout( modifier = modifier + .clickable { + themeDialog = true + } .testTag("ConstraintLayout") ) { val (title, value) = createRefs() @@ -47,7 +70,7 @@ fun SettingsThemeBox( ) Text( - text = stringResource(currentTheme.themeTextRes), + text = currentTheme.themeText, modifier = Modifier .constrainAs(value) { width = Dimension.wrapContent @@ -66,7 +89,7 @@ fun SettingsThemeBox( @Composable @DevicePreviews private fun SettingsThemeBoxPreview( - @PreviewParameter(ThemesPreviewParameterProvider::class) theme: AppTheme + @PreviewParameter(ThemePreviewParameterProvider::class) theme: AppTheme ) { MoviesTheme { SettingsThemeBox( @@ -74,7 +97,9 @@ private fun SettingsThemeBoxPreview( .fillMaxWidth() .height(48.dp) .background(MaterialTheme.colorScheme.primaryContainer), - currentTheme = theme + themes = listOf(AppTheme.FollowSystem, AppTheme.NightNo, AppTheme.NightYes), + currentTheme = theme, + onThemeSelect = {} ) } } \ No newline at end of file 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 7d29a3502..c9dbfb2fc 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 @@ -22,13 +22,13 @@ 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.settings.ktx.themeText import org.michaelbel.movies.settings_impl.R -import org.michaelbel.movies.settings.ktx.themeTextRes import org.michaelbel.movies.ui.icon.MoviesIcons import org.michaelbel.movies.ui.preview.DevicePreviews import org.michaelbel.movies.ui.theme.MoviesTheme import org.michaelbel.movies.ui.theme.model.AppTheme -import org.michaelbel.movies.ui.theme.preview.ThemesPreviewParameterProvider +import org.michaelbel.movies.ui.theme.preview.ThemePreviewParameterProvider @Composable internal fun SettingThemeDialog( @@ -115,7 +115,7 @@ private fun SettingThemeDialogContent( ) Text( - text = stringResource(theme.themeTextRes), + text = theme.themeText, modifier = Modifier .padding( start = 8.dp @@ -131,7 +131,7 @@ private fun SettingThemeDialogContent( @Composable @DevicePreviews private fun SettingThemeDialogPreview( - @PreviewParameter(ThemesPreviewParameterProvider::class) theme: AppTheme + @PreviewParameter(ThemePreviewParameterProvider::class) theme: AppTheme ) { MoviesTheme { SettingThemeDialog( 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 e45c1e024..3856f2151 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 @@ -1,7 +1,7 @@ package org.michaelbel.movies.settings.ui +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -10,6 +10,7 @@ import androidx.compose.material3.TopAppBarDefaults 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.res.stringResource import androidx.compose.ui.text.style.TextOverflow import org.michaelbel.movies.settings_impl.R @@ -37,9 +38,10 @@ internal fun SettingsToolbar( IconButton( onClick = onNavigationIconClick ) { - Icon( + Image( imageVector = MoviesIcons.ArrowBack, - contentDescription = null + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer) ) } }, diff --git a/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/lifecycle/OnLifecycleEvent.kt b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/lifecycle/OnLifecycleEvent.kt new file mode 100644 index 000000000..87d8cc0c5 --- /dev/null +++ b/feature/settings-impl/src/main/kotlin/org/michaelbel/movies/settings/ui/lifecycle/OnLifecycleEvent.kt @@ -0,0 +1,39 @@ +package org.michaelbel.movies.settings.ui.lifecycle + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner + +@Composable +private fun OnLifecycleEvent( + onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit +) { + val eventHandler = rememberUpdatedState(onEvent) + val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current) + + DisposableEffect(lifecycleOwner.value) { + val lifecycle: Lifecycle = lifecycleOwner.value.lifecycle + val observer = LifecycleEventObserver { owner, event -> + eventHandler.value(owner, event) + } + lifecycle.addObserver(observer) + onDispose { + lifecycle.removeObserver(observer) + } + } +} + +@Composable +fun OnResume( + onResume: () -> Unit +) { + OnLifecycleEvent( + onEvent = { _, event -> + onResume().takeIf { event == Lifecycle.Event.ON_RESUME } + } + ) +} \ No newline at end of file diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 25d9c7214..bcda9937d 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -24,7 +24,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.kotlin.compiler.extension.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } lint { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 00378a0b5..7a4762185 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,51 +8,54 @@ compile-sdk = "33" # @keep target-sdk = "33" # @pin -gradle = "7.4.1" +gradle = "7.4.2" # @pin update when updating compose compiler -kotlin = "1.8.0" +kotlin = "1.8.10" +kotlin-ksp = "1.8.10-1.0.9" kotlin-coroutines = "1.6.4" kotlin-serialization = "1.4.1" # @pin update when updating kotlin -kotlin-compiler-extension = "1.4.0" +compose-compiler = "1.4.3" detekt = "1.22.0" -spotless = "6.14.1" +spotless = "6.15.0" google-services = "4.3.15" play-services-ads = "21.5.0" -play-services-base = "18.1.0" +play-services-base = "18.2.0" firebase-analytics = "21.2.0" firebase-appdistribution = "3.2.0" firebase-config = "21.2.1" -firebase-crashlytics-plugin = "2.9.2" -firebase-crashlytics = "18.3.3" +firebase-crashlytics-plugin = "2.9.4" +firebase-crashlytics = "18.3.5" play-core = "1.8.1" accompanist = "0.28.0" -material = "1.7.0" +material = "1.8.0" material-compose-theme-adapter = "1.2.1" -hilt = "2.44.2" +hilt = "2.45" androidx-compose-foundation = "1.3.1" -androidx-compose-runtime = "1.3.1" -androidx-compose-ui = "1.3.1" -androidx-compose-compiler = "1.4.0" +androidx-compose-runtime = "1.3.3" +androidx-compose-ui = "1.3.3" +androidx-compose-compiler = "1.4.3" androidx-compose-material3 = "1.0.1" androidx-appcompat = "1.6.0-rc01" androidx-activity = "1.6.1" androidx-core = "1.9.0" androidx-core-splashscreen = "1.0.0" androidx-constraintlayout = "1.0.1" -androidx-lifecycle = "2.6.0-alpha03" +androidx-lifecycle = "2.6.0-rc01" androidx-hilt-navigation-compose = "1.0.0" +androidx-hilt-work = "1.0.0" androidx-navigation = "2.5.3" -androidx-paging = "1.0.0-alpha17" +androidx-paging = "1.0.0-alpha18" androidx-datastore = "1.0.0" androidx-startup = "1.1.1" androidx-room = "2.5.0" -androidx-test = "1.5.0" +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.1.1" -androidx-profile-installer = "1.3.0-alpha03" +androidx-profile-installer = "1.3.0-beta01" +androidx-work = "2.8.0" coil = "2.2.2" okhttp = "4.10.0" retrofit = "2.9.0" @@ -104,6 +107,8 @@ androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constr androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt-navigation-compose" } +androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "androidx-hilt-work" } +androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidx-hilt-work" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" } androidx-datastore-core = { module = "androidx.datastore:datastore-core", version.ref = "androidx-datastore" } @@ -122,6 +127,8 @@ androidx-benchmark-junit = { module = "androidx.benchmark:benchmark-junit4", ver androidx-benchmark-macro-junit = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark" } androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidx-test-uiautomator" } androidx-profile-installer = { module = "androidx.profileinstaller:profileinstaller", version.ref = "androidx-profile-installer" } +androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" } +androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } @@ -177,9 +184,15 @@ material = [ "material", "material-compose-theme-adapter", ] +room = [ + "androidx-room-runtime", + "androidx-room-ktx", + "androidx-room-paging", +] [plugins] kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin-ksp" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } google-services = { id = "com.google.gms.google-services", version.ref = "google-services" } firebase-appdistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebase-appdistribution" } diff --git a/readme.md b/readme.md index 8eb92f072..20dc49f25 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,3 @@ - - - - Movies = @@ -11,6 +7,12 @@ Movies Movies - easy way to discover popular movies. This is a simple TMDb client for Android +
+ + + +
+ ## Build Take a look at `local.properties` and fill it with [your own](https://developers.themoviedb.org/3/getting-started/introduction) tmdb_api_key like this: @@ -23,17 +25,6 @@ TMDB_API_KEY=your_own_tmdb_api_key [](https://play.google.com/store/apps/details?id=org.michaelbel.moviemade) [](https://github.com/michaelbel/Moviemade/releases/download/1.3.1/moviemade-v1.3.1-release.apk) -## Screenshots - -
- - - - - - -
- ## Technologies - [x] Modularization @@ -41,7 +32,7 @@ TMDB_API_KEY=your_own_tmdb_api_key - [x] [Clean Architecture](https://d.android.com/topic/architecture) - [x] [TMDB API](https://developers.themoviedb.org/3/getting-started) - [x] KTS Gradle Files -- [x] Gradle Plugin 7.4.1 +- [x] Gradle Plugin 7.4.2 - [x] Gradle Version Catalog - [x] MinSDK 21 - [x] TargetSDK 33 @@ -50,21 +41,24 @@ TMDB_API_KEY=your_own_tmdb_api_key - [x] Dark Theme - [x] Dynamic Colors - [x] [Themed App Icon](https://d.android.com/develop/ui/views/launch/icon_design_adaptive) -- [x] 100% Kotlin 1.8.0 -- [x] 100% Jetpack Compose 1.3.1, No XML +- [x] 100% Kotlin 1.8.10 +- [x] 100% Jetpack Compose 1.4.3 +- [x] Kotlin Symbol Processing API 1.8.10-1.0.9 - [x] [Accompanist](https://github.com/google/accompanist) 0.28.0 - [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) - [x] [KotlinX Coroutines](https://github.com/Kotlin/kotlinx.coroutines) 1.6.4 - [x] [KotlinX Serialization](https://github.com/Kotlin/kotlinx.serialization) 1.4.1 - [x] [Appcompat](https://d.android.com/jetpack/androidx/releases/appcompat) 1.6.0-rc01 -- [x] [Dagger Hilt](https://github.com/google/dagger) 2.44.2 +- [x] [Dagger Hilt](https://github.com/google/dagger) 2.45 - [x] [ViewModel](https://d.android.com/topic/libraries/architecture/viewmodel) - [x] [Lifecycle](https://d.android.com/topic/libraries/architecture/lifecycle) 2.6.0-alpha03 +- [x] [Room](https://d.android.com/training/data-storage/room) 2.5.0 +- [x] [WorkManager](https://d.android.com/topic/libraries/architecture/workmanager) 2.8.0 - [x] [DataStore](https://d.android.com/datastore) 1.0.0 - [x] [Startup](https://d.android.com/jetpack/androidx/releases/startup) 1.1.1 - [x] [Navigation](https://d.android.com/guide/navigation) 2.5.3 -- [x] [Paging](https://d.android.com/topic/libraries/architecture/paging/v3-overview) +- [x] [Paging](https://d.android.com/topic/libraries/architecture/paging/v3-overview) (RemoteMediator & PagingSource) 1.0.0-alpha18 - [x] [ConstraintLayout](https://d.android.com/develop/ui/views/layout/constraint-layout) - [x] [OkHttp](https://github.com/square/okhttp) 4.10.0 - [x] [Retrofit](https://github.com/square/retrofit) 2.9.0 @@ -72,7 +66,7 @@ TMDB_API_KEY=your_own_tmdb_api_key - [x] [Chucker](https://github.com/ChuckerTeam/chucker) 3.5.2 - [x] [Coil](https://github.com/coil-kt/coil) 2.2.2 - [x] [Timber](https://github.com/JakeWharton/timber) 5.0.1 -- [x] [Firebase Crashlytics](https://firebase.google.com/products/crashlytics) 18.3.3 +- [x] [Firebase Crashlytics](https://firebase.google.com/products/crashlytics) 18.3.5 - [x] [Firebase App Distribution](https://firebase.google.com/products/app-distribution) 3.2.0 - [x] [Firebase Remote Config](https://firebase.google.com/products/remote-config) 21.2.1 - [x] [Google Analytics for Firebase](https://firebase.google.com/products/analytics) 21.2.0 @@ -83,20 +77,18 @@ TMDB_API_KEY=your_own_tmdb_api_key - [x] Github Releases - [x] [Lint](https://d.android.com/studio/write/lint) - [x] [Detekt](https://github.com/detekt/detekt) 1.22.0 -- [x] [Spotless](https://github.com/diffplug/spotless) 6.14.1 +- [x] [Spotless](https://github.com/diffplug/spotless) 6.15.0 - [x] Distribute App via Telegram Bot - [x] Non-Transitive R classes - [x] [SplashScreen API](https://d.android.com/develop/ui/views/launch/splash-screen) - [x] [Per-App Language Preferences](https://d.android.com/guide/topics/resources/app-languages) - [x] [Settings Panel](https://d.android.com/reference/android/provider/Settings.Panel) - [x] [Benchmark](https://d.android.com/topic/performance/benchmarking/benchmarking-overview) -- [ ] [Room](https://d.android.com/training/data-storage/room) - [ ] Deep Links - [ ] Unit Tests - [ ] UI Tests (in progress) - [ ] [In-App Updates](https://d.android.com/guide/playcore/in-app-updates) - [ ] [Google Admob](https://developers.google.com/admob) -- [ ] [WorkManager](https://d.android.google.cn/topic/libraries/architecture/workmanager) - [ ] [KtLint](https://github.com/pinterest/ktlint) - [ ] [Support Display Cutouts](https://d.android.com/develop/ui/views/layout/display-cutout) - [ ] [Baseline Profiles](https://d.android.com/topic/performance/baselineprofiles/overview)