diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ce7cdb30a..db14482e4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(libs.appauth) implementation(libs.decompose.decompose) + implementation(libs.decompose.extensions.compose) implementation(libs.kotlinInject.runtime) ksp(libs.kotlinInject.compiler) } diff --git a/app/src/main/kotlin/com/thomaskioko/tvmaniac/MainActivity.kt b/app/src/main/kotlin/com/thomaskioko/tvmaniac/MainActivity.kt index 43ee1611b..f12ee6acc 100644 --- a/app/src/main/kotlin/com/thomaskioko/tvmaniac/MainActivity.kt +++ b/app/src/main/kotlin/com/thomaskioko/tvmaniac/MainActivity.kt @@ -9,16 +9,14 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState import com.thomaskioko.tvmaniac.compose.theme.TvManiacTheme -import com.thomaskioko.tvmaniac.datastore.api.Theme +import com.thomaskioko.tvmaniac.datastore.api.AppTheme import com.thomaskioko.tvmaniac.inject.MainActivityComponent import com.thomaskioko.tvmaniac.inject.create -import com.thomaskioko.tvmaniac.navigation.Loading -import com.thomaskioko.tvmaniac.navigation.ThemeLoaded import com.thomaskioko.tvmaniac.navigation.ThemeState class MainActivity : ComponentActivity() { @@ -36,14 +34,11 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { - val themeState by component.presenter.state.collectAsState() + val themeState by component.presenter.state.subscribeAsState() val darkTheme = shouldUseDarkTheme(themeState) splashScreen.setKeepOnScreenCondition { - when (themeState) { - Loading -> true - is ThemeLoaded -> false - } + themeState.isFetching } DisposableEffect(darkTheme) { @@ -74,13 +69,10 @@ class MainActivity : ComponentActivity() { @Composable private fun shouldUseDarkTheme( uiState: ThemeState, -): Boolean = when (uiState) { - Loading -> isSystemInDarkTheme() - is ThemeLoaded -> when (uiState.theme) { - Theme.LIGHT -> false - Theme.DARK -> true - Theme.SYSTEM -> isSystemInDarkTheme() - } +): Boolean = when (uiState.appTheme) { + AppTheme.LIGHT_THEME -> false + AppTheme.DARK_THEME -> true + AppTheme.SYSTEM_THEME -> isSystemInDarkTheme() } /** diff --git a/core/datastore/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/api/DatastoreRepository.kt b/core/datastore/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/api/DatastoreRepository.kt index 32d4ae523..ee07d4823 100644 --- a/core/datastore/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/api/DatastoreRepository.kt +++ b/core/datastore/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/api/DatastoreRepository.kt @@ -3,8 +3,8 @@ package com.thomaskioko.tvmaniac.datastore.api import kotlinx.coroutines.flow.Flow interface DatastoreRepository { - fun saveTheme(theme: Theme) - fun observeTheme(): Flow + fun saveTheme(appTheme: AppTheme) + fun observeTheme(): Flow fun clearAuthState() fun observeAuthState(): Flow @@ -12,8 +12,8 @@ interface DatastoreRepository { suspend fun getAuthState(): AuthState? } -enum class Theme { - LIGHT, - DARK, - SYSTEM, +enum class AppTheme(val value: String) { + LIGHT_THEME("Light Theme"), + DARK_THEME("Light Theme"), + SYSTEM_THEME("Light Theme"), } diff --git a/core/datastore/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DatastoreRepositoryImpl.kt b/core/datastore/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DatastoreRepositoryImpl.kt index f85a4482e..a36389ce7 100644 --- a/core/datastore/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DatastoreRepositoryImpl.kt +++ b/core/datastore/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DatastoreRepositoryImpl.kt @@ -5,9 +5,9 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey +import com.thomaskioko.tvmaniac.datastore.api.AppTheme import com.thomaskioko.tvmaniac.datastore.api.AuthState import com.thomaskioko.tvmaniac.datastore.api.DatastoreRepository -import com.thomaskioko.tvmaniac.datastore.api.Theme import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -21,19 +21,19 @@ class DatastoreRepositoryImpl( private val dataStore: DataStore, ) : DatastoreRepository { - override fun saveTheme(theme: Theme) { + override fun saveTheme(appTheme: AppTheme) { coroutineScope.io.launch { dataStore.edit { preferences -> - preferences[KEY_THEME] = theme.name + preferences[KEY_THEME] = appTheme.name } } } - override fun observeTheme(): Flow = dataStore.data.map { preferences -> + override fun observeTheme(): Flow = dataStore.data.map { preferences -> when (preferences[KEY_THEME]) { - Theme.LIGHT.name -> Theme.LIGHT - Theme.DARK.name -> Theme.DARK - else -> Theme.SYSTEM + AppTheme.LIGHT_THEME.name -> AppTheme.LIGHT_THEME + AppTheme.DARK_THEME.name -> AppTheme.DARK_THEME + else -> AppTheme.SYSTEM_THEME } } diff --git a/core/datastore/implementation/src/commonTest/kotlin/com/thomaskioko/tvmaniac/datastore/implemetation/DatastoreRepositoryImplTest.kt b/core/datastore/implementation/src/commonTest/kotlin/com/thomaskioko/tvmaniac/datastore/implemetation/DatastoreRepositoryImplTest.kt index 149b6dfcb..ae96fd88a 100644 --- a/core/datastore/implementation/src/commonTest/kotlin/com/thomaskioko/tvmaniac/datastore/implemetation/DatastoreRepositoryImplTest.kt +++ b/core/datastore/implementation/src/commonTest/kotlin/com/thomaskioko/tvmaniac/datastore/implemetation/DatastoreRepositoryImplTest.kt @@ -5,7 +5,7 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import app.cash.turbine.test -import com.thomaskioko.tvmaniac.datastore.api.Theme +import com.thomaskioko.tvmaniac.datastore.api.AppTheme import com.thomaskioko.tvmaniac.datastore.implementation.DatastoreRepositoryImpl import com.thomaskioko.tvmaniac.datastore.implementation.DatastoreRepositoryImpl.Companion.KEY_THEME import com.thomaskioko.tvmaniac.datastore.implementation.IgnoreIos @@ -56,7 +56,7 @@ class DatastoreRepositoryImplTest { @Test fun default_theme_is_emitted() = runTest { repository.observeTheme().test { - awaitItem() shouldBe Theme.SYSTEM + awaitItem() shouldBe AppTheme.SYSTEM_THEME } } @@ -64,9 +64,9 @@ class DatastoreRepositoryImplTest { @Test fun when_theme_is_changed_correct_value_is_set() = runTest { repository.observeTheme().test { - repository.saveTheme(Theme.DARK) - awaitItem() shouldBe Theme.SYSTEM // Default theme - awaitItem() shouldBe Theme.DARK + repository.saveTheme(AppTheme.DARK_THEME) + awaitItem() shouldBe AppTheme.SYSTEM_THEME // Default theme + awaitItem() shouldBe AppTheme.DARK_THEME } } } diff --git a/core/datastore/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/testing/FakeDatastoreRepository.kt b/core/datastore/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/testing/FakeDatastoreRepository.kt index 8cb1abca6..b42fd4a38 100644 --- a/core/datastore/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/testing/FakeDatastoreRepository.kt +++ b/core/datastore/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/testing/FakeDatastoreRepository.kt @@ -1,30 +1,30 @@ package com.thomaskioko.tvmaniac.datastore.testing +import com.thomaskioko.tvmaniac.datastore.api.AppTheme import com.thomaskioko.tvmaniac.datastore.api.AuthState import com.thomaskioko.tvmaniac.datastore.api.DatastoreRepository -import com.thomaskioko.tvmaniac.datastore.api.Theme import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow class FakeDatastoreRepository : DatastoreRepository { - private val themeFlow: Channel = Channel(Channel.UNLIMITED) + private val appThemeFlow: Channel = Channel(Channel.UNLIMITED) private val authStateFlow: Channel = Channel(Channel.UNLIMITED) - suspend fun setTheme(theme: Theme) { - themeFlow.send(theme) + suspend fun setTheme(appTheme: AppTheme) { + appThemeFlow.send(appTheme) } suspend fun setAuthState(authState: AuthState) { authStateFlow.send(authState) } - override fun saveTheme(theme: Theme) { + override fun saveTheme(appTheme: AppTheme) { // no -op } - override fun observeTheme(): Flow = themeFlow.receiveAsFlow() + override fun observeTheme(): Flow = appThemeFlow.receiveAsFlow() override fun clearAuthState() { // no -op diff --git a/core/trakt-auth/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthComponent.kt b/core/trakt-auth/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthComponent.kt index 8abed14e4..b010adcc6 100644 --- a/core/trakt-auth/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthComponent.kt +++ b/core/trakt-auth/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthComponent.kt @@ -3,9 +3,7 @@ package com.thomaskioko.tvmaniac.traktauth.implementation import android.app.Application import android.net.Uri import androidx.core.net.toUri -import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager import com.thomaskioko.tvmaniac.util.model.Configs -import com.thomaskioko.tvmaniac.util.scope.ActivityScope import com.thomaskioko.tvmaniac.util.scope.ApplicationScope import me.tatarka.inject.annotations.Provides import net.openid.appauth.AuthorizationRequest @@ -51,10 +49,3 @@ interface TraktAuthComponent { application: Application, ): AuthorizationService = AuthorizationService(application) } - -interface TraktAuthManagerComponent { - - @ActivityScope - @Provides - fun provideTraktAuthManager(bind: TraktAuthManagerImpl): TraktAuthManager = bind -} diff --git a/core/trakt-auth/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt b/core/trakt-auth/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt index 4b7a18add..d2a6701b5 100644 --- a/core/trakt-auth/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt +++ b/core/trakt-auth/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt @@ -11,7 +11,7 @@ import net.openid.appauth.AuthorizationService import net.openid.appauth.ClientAuthentication @Inject -class TraktAuthManagerImpl( +actual class TraktAuthManagerImpl( private val activity: ComponentActivity, private val traktActivityResultContract: TraktActivityResultContract, private val traktAuthRepository: TraktAuthRepository, diff --git a/core/trakt-auth/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerComponent.kt b/core/trakt-auth/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerComponent.kt new file mode 100644 index 000000000..7d4b387d1 --- /dev/null +++ b/core/trakt-auth/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerComponent.kt @@ -0,0 +1,14 @@ +package com.thomaskioko.tvmaniac.traktauth.implementation + +import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager +import com.thomaskioko.tvmaniac.util.scope.ActivityScope +import me.tatarka.inject.annotations.Provides + +interface TraktAuthManagerComponent { + + @ActivityScope + @Provides + fun provideTraktAuthManager( + bind: TraktAuthManagerImpl, + ): TraktAuthManager = bind +} diff --git a/core/trakt-auth/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt b/core/trakt-auth/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt new file mode 100644 index 000000000..a8365c583 --- /dev/null +++ b/core/trakt-auth/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt @@ -0,0 +1,7 @@ +package com.thomaskioko.tvmaniac.traktauth.implementation + +import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager +import me.tatarka.inject.annotations.Inject + +@Inject +expect class TraktAuthManagerImpl : TraktAuthManager diff --git a/core/trakt-auth/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt b/core/trakt-auth/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt new file mode 100644 index 000000000..bd2e096a6 --- /dev/null +++ b/core/trakt-auth/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt @@ -0,0 +1,17 @@ +package com.thomaskioko.tvmaniac.traktauth.implementation + +import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager +import me.tatarka.inject.annotations.Inject + +// TODO:: Replace with actual typealias. See https://youtrack.jetbrains.com/issue/KT-61573 +@Inject +actual class TraktAuthManagerImpl : TraktAuthManager { + + override fun launchWebView() { + // NO OP + } + + override fun registerResult() { + // NO OP + } +} diff --git a/core/trakt-auth/implementation/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt b/core/trakt-auth/implementation/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt new file mode 100644 index 000000000..3f06b3b3f --- /dev/null +++ b/core/trakt-auth/implementation/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/traktauth/implementation/TraktAuthManagerImpl.kt @@ -0,0 +1,15 @@ +package com.thomaskioko.tvmaniac.traktauth.implementation + +import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager +import me.tatarka.inject.annotations.Inject + +@Inject +actual class TraktAuthManagerImpl : TraktAuthManager { + override fun launchWebView() { + // NO OP + } + + override fun registerResult() { + // NO OP + } +} diff --git a/core/util/build.gradle.kts b/core/util/build.gradle.kts index 242c614ca..915124382 100644 --- a/core/util/build.gradle.kts +++ b/core/util/build.gradle.kts @@ -19,6 +19,7 @@ kotlin { api(libs.ktor.serialization) implementation(libs.coroutines.core) + implementation(libs.decompose.decompose) implementation(libs.kermit) implementation(libs.napier) implementation(libs.kotlinInject.runtime) diff --git a/core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/decompose/DecomposeUtils.kt b/core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/decompose/DecomposeUtils.kt new file mode 100644 index 000000000..78a6a4e72 --- /dev/null +++ b/core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/decompose/DecomposeUtils.kt @@ -0,0 +1,65 @@ +package com.thomaskioko.tvmaniac.util.decompose + +import com.arkivanov.decompose.value.MutableValue +import com.arkivanov.decompose.value.Value +import com.arkivanov.essenty.lifecycle.Lifecycle +import com.arkivanov.essenty.lifecycle.LifecycleOwner +import com.arkivanov.essenty.lifecycle.doOnDestroy +import com.arkivanov.essenty.lifecycle.subscribe +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +/** + * This helper implementation in from Cofetti Kmp App + * See https://github.com/joreilly/Confetti/blob/fb832c2131b2f3e5276a1a3a30666aa571e1e17e/shared/src/commonMain/kotlin/dev/johnoreilly/confetti/decompose/DecomposeUtils.kt#L27 + */ + +fun LifecycleOwner.coroutineScope( + context: CoroutineContext = Dispatchers.Main.immediate, +): CoroutineScope { + val scope = CoroutineScope(context + SupervisorJob()) + lifecycle.doOnDestroy(scope::cancel) + + return scope +} + +fun StateFlow.asValue( + lifecycle: Lifecycle, + context: CoroutineContext = Dispatchers.Main.immediate, +): Value = + asValue( + initialValue = value, + lifecycle = lifecycle, + context = context, + ) + +fun Flow.asValue( + initialValue: T, + lifecycle: Lifecycle, + context: CoroutineContext = Dispatchers.Main.immediate, +): Value { + val value = MutableValue(initialValue) + var scope: CoroutineScope? = null + + lifecycle.subscribe( + onStart = { + scope = CoroutineScope(context).apply { + launch { + collect { value.value = it } + } + } + }, + onStop = { + scope?.cancel() + scope = null + }, + ) + + return value +} diff --git a/feature/discover/build.gradle.kts b/feature/discover/build.gradle.kts index bd70330fe..b0816bcef 100644 --- a/feature/discover/build.gradle.kts +++ b/feature/discover/build.gradle.kts @@ -17,5 +17,6 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.ui.util) implementation(libs.androidx.compose.runtime) + implementation(libs.decompose.extensions.compose) implementation(libs.snapper) } diff --git a/feature/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt b/feature/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt index 3f3b6d467..53499306c 100644 --- a/feature/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt +++ b/feature/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt @@ -40,7 +40,6 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow @@ -53,6 +52,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState import com.thomaskioko.tvmaniac.category.api.model.Category import com.thomaskioko.tvmaniac.compose.components.BoxTextItems import com.thomaskioko.tvmaniac.compose.components.ErrorUi @@ -89,7 +89,7 @@ fun DiscoverScreen( discoverShowsPresenter: DiscoverShowsPresenter, modifier: Modifier = Modifier, ) { - val discoverState by discoverShowsPresenter.state.collectAsState() + val discoverState by discoverShowsPresenter.state.subscribeAsState() val pagerState = rememberPagerState(pageCount = { (discoverState as? DataLoaded)?.recommendedShows?.size ?: 0 }) diff --git a/feature/library/build.gradle.kts b/feature/library/build.gradle.kts index 7b4f0b570..108628f86 100644 --- a/feature/library/build.gradle.kts +++ b/feature/library/build.gradle.kts @@ -16,5 +16,6 @@ dependencies { implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.runtime) + implementation(libs.decompose.extensions.compose) implementation(libs.kotlinx.collections) } diff --git a/feature/library/src/main/kotlin/com/thomaskioko/tvmaniac/library/LibraryScreen.kt b/feature/library/src/main/kotlin/com/thomaskioko/tvmaniac/library/LibraryScreen.kt index 065e4d6a4..28fdf6deb 100644 --- a/feature/library/src/main/kotlin/com/thomaskioko/tvmaniac/library/LibraryScreen.kt +++ b/feature/library/src/main/kotlin/com/thomaskioko/tvmaniac/library/LibraryScreen.kt @@ -9,13 +9,13 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState import com.thomaskioko.tvmaniac.compose.components.EmptyContent import com.thomaskioko.tvmaniac.compose.components.ErrorUi import com.thomaskioko.tvmaniac.compose.components.LazyGridItems @@ -27,10 +27,10 @@ import com.thomaskioko.tvmaniac.presentation.watchlist.ErrorLoadingShows import com.thomaskioko.tvmaniac.presentation.watchlist.LibraryAction import com.thomaskioko.tvmaniac.presentation.watchlist.LibraryContent import com.thomaskioko.tvmaniac.presentation.watchlist.LibraryPresenter +import com.thomaskioko.tvmaniac.presentation.watchlist.LibraryShowClicked import com.thomaskioko.tvmaniac.presentation.watchlist.LibraryState import com.thomaskioko.tvmaniac.presentation.watchlist.LoadingShows import com.thomaskioko.tvmaniac.presentation.watchlist.ReloadLibrary -import com.thomaskioko.tvmaniac.presentation.watchlist.ShowClicked import com.thomaskioko.tvmaniac.presentation.watchlist.model.LibraryItem import com.thomaskioko.tvmaniac.resources.R import kotlinx.collections.immutable.ImmutableList @@ -40,7 +40,7 @@ fun LibraryScreen( presenter: LibraryPresenter, modifier: Modifier = Modifier, ) { - val libraryState by presenter.state.collectAsState() + val libraryState by presenter.state.subscribeAsState() LibraryScreen( modifier = modifier, @@ -87,7 +87,7 @@ internal fun LibraryScreen( else -> LibraryGridContent( list = state.list, paddingValues = contentPadding, - onItemClicked = { onAction(ShowClicked(it)) }, + onItemClicked = { onAction(LibraryShowClicked(it)) }, ) } } diff --git a/feature/more-shows/build.gradle.kts b/feature/more-shows/build.gradle.kts index 972aaea0b..89f56d5ee 100644 --- a/feature/more-shows/build.gradle.kts +++ b/feature/more-shows/build.gradle.kts @@ -15,4 +15,5 @@ dependencies { implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.runtime) + implementation(libs.decompose.extensions.compose) } diff --git a/feature/more-shows/src/main/kotlin/com/thomaskioko/tvmaniac/feature/moreshows/MoreShowsScreen.kt b/feature/more-shows/src/main/kotlin/com/thomaskioko/tvmaniac/feature/moreshows/MoreShowsScreen.kt index a6085f10f..2d27e8333 100644 --- a/feature/more-shows/src/main/kotlin/com/thomaskioko/tvmaniac/feature/moreshows/MoreShowsScreen.kt +++ b/feature/more-shows/src/main/kotlin/com/thomaskioko/tvmaniac/feature/moreshows/MoreShowsScreen.kt @@ -22,13 +22,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState import com.thomaskioko.tvmaniac.compose.components.AsyncImageComposable import com.thomaskioko.tvmaniac.compose.components.ThemePreviews import com.thomaskioko.tvmaniac.compose.components.TvManiacTopBar @@ -48,7 +48,7 @@ fun MoreShowsScreen( presenter: MoreShowsPresenter, modifier: Modifier = Modifier, ) { - val state by presenter.state.collectAsState() + val state by presenter.state.subscribeAsState() MoreShowsScreen( modifier = modifier, diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts index 8c2dfd07c..b3e182eb4 100644 --- a/feature/profile/build.gradle.kts +++ b/feature/profile/build.gradle.kts @@ -15,5 +15,6 @@ dependencies { implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.runtime) + implementation(libs.decompose.extensions.compose) implementation(libs.snapper) } diff --git a/feature/profile/src/main/kotlin/com/thomaskioko/tvmaniac/profile/ProfileScreen.kt b/feature/profile/src/main/kotlin/com/thomaskioko/tvmaniac/profile/ProfileScreen.kt index fcfb537bc..2e842969a 100644 --- a/feature/profile/src/main/kotlin/com/thomaskioko/tvmaniac/profile/ProfileScreen.kt +++ b/feature/profile/src/main/kotlin/com/thomaskioko/tvmaniac/profile/ProfileScreen.kt @@ -34,7 +34,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,6 +46,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState import com.thomaskioko.tvmaniac.compose.components.AsyncImageComposable import com.thomaskioko.tvmaniac.compose.components.BasicDialog import com.thomaskioko.tvmaniac.compose.components.ThemePreviews @@ -72,7 +72,7 @@ fun ProfileScreen( presenter: ProfilePresenter, modifier: Modifier = Modifier, ) { - val state by presenter.state.collectAsState() + val state by presenter.state.subscribeAsState() ProfileScreen( state = state, diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts index 2a497bb4a..013e2b93a 100644 --- a/feature/search/build.gradle.kts +++ b/feature/search/build.gradle.kts @@ -15,4 +15,5 @@ dependencies { implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.runtime) + implementation(libs.decompose.extensions.compose) } diff --git a/feature/season-details/build.gradle.kts b/feature/season-details/build.gradle.kts index 64a3fe8d3..7bfada708 100644 --- a/feature/season-details/build.gradle.kts +++ b/feature/season-details/build.gradle.kts @@ -17,5 +17,6 @@ dependencies { implementation(libs.androidx.compose.material.icons) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.runtime) + implementation(libs.decompose.extensions.compose) implementation(libs.snapper) } diff --git a/feature/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonDetailsScreen.kt b/feature/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonDetailsScreen.kt index 9e1218ebf..ceb3063f6 100644 --- a/feature/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonDetailsScreen.kt +++ b/feature/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonDetailsScreen.kt @@ -23,7 +23,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.toMutableStateList @@ -32,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState import com.thomaskioko.tvmaniac.compose.components.ErrorUi import com.thomaskioko.tvmaniac.compose.components.LoadingIndicator import com.thomaskioko.tvmaniac.compose.components.ThemePreviews @@ -60,7 +60,7 @@ fun SeasonDetailsScreen( presenter: SeasonDetailsPresenter, modifier: Modifier = Modifier, ) { - val state by presenter.state.collectAsState() + val state by presenter.state.subscribeAsState() SeasonDetailsScreen( modifier = modifier, diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 72a6351b3..31468cc7b 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.runtime) - + implementation(libs.decompose.extensions.compose) implementation(libs.kotlinx.collections) } diff --git a/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsExtensions.kt b/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsExtensions.kt index 10835e9a6..f4e6f7c23 100644 --- a/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsExtensions.kt +++ b/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsExtensions.kt @@ -2,13 +2,13 @@ package com.thomaskioko.tvmaniac.settings import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable -import com.thomaskioko.tvmaniac.datastore.api.Theme +import com.thomaskioko.tvmaniac.datastore.api.AppTheme @Composable -fun Theme.shouldUseDarkColors(): Boolean { +fun AppTheme.shouldUseDarkColors(): Boolean { return when (this) { - Theme.LIGHT -> false - Theme.DARK -> true + AppTheme.LIGHT_THEME -> false + AppTheme.DARK_THEME -> true else -> isSystemInDarkTheme() } } diff --git a/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsPreviewParameterProvider.kt b/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsPreviewParameterProvider.kt index c7651096c..c5676cb44 100644 --- a/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsPreviewParameterProvider.kt +++ b/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsPreviewParameterProvider.kt @@ -1,7 +1,7 @@ package com.thomaskioko.tvmaniac.settings import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.thomaskioko.tvmaniac.datastore.api.Theme +import com.thomaskioko.tvmaniac.datastore.api.AppTheme import com.thomaskioko.tvmaniac.presentation.settings.SettingsState import com.thomaskioko.tvmaniac.presentation.settings.UserInfo @@ -10,7 +10,7 @@ class SettingsPreviewParameterProvider : PreviewParameterProvider get() { return sequenceOf( SettingsState( - theme = Theme.DARK, + appTheme = AppTheme.DARK_THEME, isLoading = false, showthemePopup = false, showTraktDialog = false, @@ -24,7 +24,7 @@ class SettingsPreviewParameterProvider : PreviewParameterProvider ), ), SettingsState( - theme = Theme.DARK, + appTheme = AppTheme.DARK_THEME, isLoading = false, showthemePopup = false, showTraktDialog = false, diff --git a/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsScreen.kt b/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsScreen.kt index 05ebdbe2d..c65e7c65c 100644 --- a/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/thomaskioko/tvmaniac/settings/SettingsScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -49,12 +48,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState import com.thomaskioko.tvmaniac.compose.components.AsyncImageComposable import com.thomaskioko.tvmaniac.compose.components.BasicDialog import com.thomaskioko.tvmaniac.compose.components.ThemePreviews import com.thomaskioko.tvmaniac.compose.components.TvManiacTopBar import com.thomaskioko.tvmaniac.compose.theme.TvManiacTheme -import com.thomaskioko.tvmaniac.datastore.api.Theme +import com.thomaskioko.tvmaniac.datastore.api.AppTheme import com.thomaskioko.tvmaniac.presentation.settings.ChangeThemeClicked import com.thomaskioko.tvmaniac.presentation.settings.DismissThemeClicked import com.thomaskioko.tvmaniac.presentation.settings.DismissTraktDialog @@ -73,7 +73,7 @@ fun SettingsScreen( presenter: SettingsPresenter, modifier: Modifier = Modifier, ) { - val state by presenter.state.collectAsState() + val state by presenter.state.subscribeAsState() val snackbarHostState = remember { SnackbarHostState() } SettingsScreen( @@ -117,7 +117,7 @@ internal fun SettingsScreen( SettingsScreen( userInfo = state.userInfo, - theme = state.theme, + appTheme = state.appTheme, showPopup = state.showthemePopup, showTraktDialog = state.showTraktDialog, isLoading = state.isLoading, @@ -133,7 +133,7 @@ internal fun SettingsScreen( @Composable fun SettingsScreen( userInfo: UserInfo?, - theme: Theme, + appTheme: AppTheme, showPopup: Boolean, showTraktDialog: Boolean, isLoading: Boolean, @@ -160,7 +160,7 @@ fun SettingsScreen( item { SettingsThemeItem( showPopup = showPopup, - theme = theme, + appTheme = appTheme, onThemeSelected = { onAction(ThemeSelected(it)) }, onThemeClicked = { onAction(ChangeThemeClicked) }, onDismissTheme = { onAction(DismissThemeClicked) }, @@ -309,16 +309,16 @@ fun TrackDialog( @Composable private fun SettingsThemeItem( - theme: Theme, + appTheme: AppTheme, showPopup: Boolean, - onThemeSelected: (Theme) -> Unit, + onThemeSelected: (AppTheme) -> Unit, onThemeClicked: () -> Unit, onDismissTheme: () -> Unit, ) { - val themeTitle = when (theme) { - Theme.LIGHT -> stringResource(R.string.settings_title_theme_dark) - Theme.DARK -> stringResource(R.string.settings_title_theme_light) - Theme.SYSTEM -> stringResource(R.string.settings_title_theme_system) + val appThemeTitle = when (appTheme) { + AppTheme.LIGHT_THEME -> stringResource(R.string.settings_title_theme_dark) + AppTheme.DARK_THEME -> stringResource(R.string.settings_title_theme_light) + AppTheme.SYSTEM_THEME -> stringResource(R.string.settings_title_theme_system) } Column( @@ -354,13 +354,13 @@ private fun SettingsThemeItem( .padding(end = 8.dp, bottom = 8.dp) .weight(1f), ) { - TitleItem(themeTitle) + TitleItem(appThemeTitle) SettingDescription(stringResource(R.string.settings_theme_description)) } ThemeMenu( isVisible = showPopup, - selectedTheme = theme, + selectedAppTheme = appTheme, onDismissTheme = onDismissTheme, onThemeSelected = onThemeSelected, ) @@ -375,9 +375,9 @@ private fun SettingsThemeItem( @Composable private fun ThemeMenu( isVisible: Boolean, - selectedTheme: Theme, + selectedAppTheme: AppTheme, onDismissTheme: () -> Unit, - onThemeSelected: (Theme) -> Unit, + onThemeSelected: (AppTheme) -> Unit, ) { AnimatedVisibility( visible = isVisible, @@ -399,22 +399,22 @@ private fun ThemeMenu( ) { ThemeMenuItem( - theme = Theme.LIGHT, - selectedTheme = selectedTheme, + appTheme = AppTheme.LIGHT_THEME, + selectedAppTheme = selectedAppTheme, onThemeSelected = onThemeSelected, onDismissTheme = onDismissTheme, ) ThemeMenuItem( - theme = Theme.DARK, - selectedTheme = selectedTheme, + appTheme = AppTheme.DARK_THEME, + selectedAppTheme = selectedAppTheme, onThemeSelected = onThemeSelected, onDismissTheme = onDismissTheme, ) ThemeMenuItem( - theme = Theme.SYSTEM, - selectedTheme = selectedTheme, + appTheme = AppTheme.SYSTEM_THEME, + selectedAppTheme = selectedAppTheme, onThemeSelected = onThemeSelected, onDismissTheme = onDismissTheme, ) @@ -424,19 +424,19 @@ private fun ThemeMenu( @Composable private fun ThemeMenuItem( - theme: Theme, - selectedTheme: Theme, - onThemeSelected: (Theme) -> Unit, + appTheme: AppTheme, + selectedAppTheme: AppTheme, + onThemeSelected: (AppTheme) -> Unit, onDismissTheme: () -> Unit, ) { - val themeTitle = when (theme) { - Theme.LIGHT -> "Light Theme" - Theme.DARK -> "Dark Theme" - Theme.SYSTEM -> "System Theme" + val appThemeTitle = when (appTheme) { + AppTheme.LIGHT_THEME -> "Light Theme" + AppTheme.DARK_THEME -> "Dark Theme" + AppTheme.SYSTEM_THEME -> "System Theme" } DropdownMenuItem( onClick = { - onThemeSelected(theme) + onThemeSelected(appTheme) onDismissTheme() }, text = { @@ -448,18 +448,18 @@ private fun ThemeMenuItem( horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = themeTitle, + text = appThemeTitle, modifier = Modifier .weight(1f), ) RadioButton( - selected = selectedTheme == theme, + selected = selectedAppTheme == appTheme, colors = RadioButtonDefaults.colors( selectedColor = MaterialTheme.colorScheme.secondary, ), onClick = { - onThemeSelected(theme) + onThemeSelected(appTheme) onDismissTheme() }, ) diff --git a/feature/show-details/build.gradle.kts b/feature/show-details/build.gradle.kts index a2f2447ae..50314e10f 100644 --- a/feature/show-details/build.gradle.kts +++ b/feature/show-details/build.gradle.kts @@ -17,5 +17,6 @@ dependencies { implementation(libs.androidx.compose.material.icons) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.runtime) + implementation(libs.decompose.extensions.compose) implementation(libs.snapper) } diff --git a/feature/show-details/src/main/kotlin/com/thomaskioko/showdetails/ShowDetailScreen.kt b/feature/show-details/src/main/kotlin/com/thomaskioko/showdetails/ShowDetailScreen.kt index 1ec082bd4..d3e27d4cf 100644 --- a/feature/show-details/src/main/kotlin/com/thomaskioko/showdetails/ShowDetailScreen.kt +++ b/feature/show-details/src/main/kotlin/com/thomaskioko/showdetails/ShowDetailScreen.kt @@ -41,7 +41,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Tab import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -67,6 +66,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState import com.thomaskioko.showdetails.DetailConstants.HEADER_HEIGHT import com.thomaskioko.tvmaniac.compose.components.AsyncImageComposable import com.thomaskioko.tvmaniac.compose.components.CollapsableAppBar @@ -82,11 +82,11 @@ import com.thomaskioko.tvmaniac.compose.components.TvPosterCard import com.thomaskioko.tvmaniac.compose.extensions.copy import com.thomaskioko.tvmaniac.compose.theme.TvManiacTheme import com.thomaskioko.tvmaniac.compose.theme.backgroundGradient -import com.thomaskioko.tvmaniac.presentation.showdetails.BackClicked +import com.thomaskioko.tvmaniac.presentation.showdetails.DetailBackClicked +import com.thomaskioko.tvmaniac.presentation.showdetails.DetailShowClicked import com.thomaskioko.tvmaniac.presentation.showdetails.DismissWebViewError import com.thomaskioko.tvmaniac.presentation.showdetails.FollowShowClicked import com.thomaskioko.tvmaniac.presentation.showdetails.SeasonClicked -import com.thomaskioko.tvmaniac.presentation.showdetails.ShowClicked import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsAction import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsPresenter import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsState @@ -105,7 +105,7 @@ fun ShowDetailsScreen( presenter: ShowDetailsPresenter, modifier: Modifier = Modifier, ) { - val state by presenter.state.collectAsState() + val state by presenter.state.subscribeAsState() val snackbarHostState = remember { SnackbarHostState() } val listState = rememberLazyListState() @@ -134,7 +134,7 @@ internal fun ShowDetailsScreen( ShowTopBar( listState = listState, title = title, - onNavUpClick = { onAction(BackClicked) }, + onNavUpClick = { onAction(DetailBackClicked) }, ) }, snackbarHost = { @@ -207,7 +207,7 @@ private fun ShowDetailsContent( SimilarShowsContent( isLoading = similarShowsContent.isLoading, similarShows = similarShowsContent.similarShows, - onShowClicked = { onAction(ShowClicked(it)) }, + onShowClicked = { onAction(DetailShowClicked(it)) }, ) } } diff --git a/feature/trailers/build.gradle.kts b/feature/trailers/build.gradle.kts index 9ed4fd5ad..ff03ae0df 100644 --- a/feature/trailers/build.gradle.kts +++ b/feature/trailers/build.gradle.kts @@ -16,5 +16,6 @@ dependencies { implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.runtime) + implementation(libs.decompose.extensions.compose) implementation(libs.youtubePlayer) } \ No newline at end of file diff --git a/feature/trailers/src/main/kotlin/com/thomaskioko/tvmaniac/videoplayer/TrailersScreen.kt b/feature/trailers/src/main/kotlin/com/thomaskioko/tvmaniac/videoplayer/TrailersScreen.kt index 1b39192f6..6c8ab7c57 100644 --- a/feature/trailers/src/main/kotlin/com/thomaskioko/tvmaniac/videoplayer/TrailersScreen.kt +++ b/feature/trailers/src/main/kotlin/com/thomaskioko/tvmaniac/videoplayer/TrailersScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -38,6 +37,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.PlayerConstants import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener @@ -66,7 +66,7 @@ fun TrailersScreen( presenter: TrailersPresenter, modifier: Modifier = Modifier, ) { - val state by presenter.state.collectAsState() + val state by presenter.state.subscribeAsState() TrailersScreen( modifier = modifier, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 713bc6e74..db45ac959 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,4 @@ [versions] -accompanist = "0.33.2-alpha" agp = "8.1.4" androidx-activity = "1.8.1" androidx-browser = "1.7.0" @@ -16,11 +15,11 @@ compose-bom = "2023.10.01" compose-constraintlayout = "1.0.1" composecompiler = "1.5.4" coroutines = "1.7.3" -datetime = "0.4.1" -decompose = "2.2.0-compose-experimental-beta02" -decompose-beta = "2.2.0-beta02" +datetime = "0.5.0" +decompose = "2.2.0" dependency-analysis = "1.25.0" dependency-check = "0.50.0" +essenty = "1.3.0" desugar = "2.0.4" kenburns = "1.0.7" kermit = "1.2.3" @@ -33,7 +32,7 @@ ksp = "1.9.20-1.0.13" ktor = "2.3.6" lint = "1.2.0" napier = "2.6.1" -shared-module-version = "0.8.1" +shared-module-version = "0.9.0" snapper = "0.3.0" sqldelight = "2.0.0" store5 = "5.0.0" @@ -42,8 +41,6 @@ yamlkt = "0.12.0" youtubePlayer = "12.0.0" [libraries] -accompanist-navigation-material = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanist" } - androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } androidx-compose-material-icons = { module = "androidx.compose.material:material-icons-extended" } @@ -73,7 +70,8 @@ coroutines-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } decompose-decompose = { group = "com.arkivanov.decompose", name = "decompose", version.ref = "decompose" } -decompose-extensions-compose = { group = "com.arkivanov.decompose", name = "extensions-compose-jetpack", version.ref = "decompose-beta" } +decompose-extensions-compose = { group = "com.arkivanov.decompose", name = "extensions-compose-jetpack", version.ref = "decompose" } +essenty-lifecycle = { module = "com.arkivanov.essenty:lifecycle", version.ref = "essenty" } kenburns = { module = "com.flaviofaria:kenburnsview", version.ref = "kenburns" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } diff --git a/ios/ios/AppDelegate.swift b/ios/ios/AppDelegate.swift new file mode 100644 index 000000000..ff10abff5 --- /dev/null +++ b/ios/ios/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// tv-maniac +// +// Created by Thomas Kioko on 03.12.23. +// Copyright © 2023 orgName. All rights reserved. +// + +import SwiftUI +import UIKit +import TvManiac + +class AppDelegate: NSObject, UIApplicationDelegate { + + @State var themeAppTheme: AppTheme = AppTheme.systemTheme + + let applicationComponent: InjectApplicationComponent + let rootHolder: RootHolder + let iosViewPresenter :InjectIosViewPresenterComponent + + override init() { + rootHolder = RootHolder() + applicationComponent = InjectApplicationComponent() + + iosViewPresenter = InjectIosViewPresenterComponent( + componentContext: DefaultComponentContext( + lifecycle: rootHolder.lifecycle, + stateKeeper: nil, + instanceKeeper: nil, + backHandler: nil + ), + applicationComponent: applicationComponent + ) + } + +} diff --git a/ios/ios/Assets.xcassets/Colors/blue.colorset/Contents.json b/ios/ios/Assets.xcassets/Colors/blue.colorset/Contents.json new file mode 100644 index 000000000..c94c22625 --- /dev/null +++ b/ios/ios/Assets.xcassets/Colors/blue.colorset/Contents.json @@ -0,0 +1,29 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xC7", + "green" : "0x49", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/ios/Ui/Components/AsyncImage.swift b/ios/ios/Components/AsyncImage.swift similarity index 100% rename from ios/ios/Ui/Components/AsyncImage.swift rename to ios/ios/Components/AsyncImage.swift diff --git a/ios/ios/Ui/Components/BlurView.swift b/ios/ios/Components/BlurView.swift similarity index 81% rename from ios/ios/Ui/Components/BlurView.swift rename to ios/ios/Components/BlurView.swift index 974268ae0..7e1d3c37a 100644 --- a/ios/ios/Ui/Components/BlurView.swift +++ b/ios/ios/Components/BlurView.swift @@ -6,13 +6,15 @@ import SwiftUI struct BlurView: UIViewRepresentable { + + var style: UIBlurEffect.Style func makeUIView(context: Context) -> UIVisualEffectView { UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterialDark)) } func updateUIView(_ uiView: UIVisualEffectView, context: Context) { - + uiView.effect = UIBlurEffect(style: style) } } diff --git a/ios/ios/Ui/Components/BorderedButton.swift b/ios/ios/Components/BorderedButton.swift similarity index 100% rename from ios/ios/Ui/Components/BorderedButton.swift rename to ios/ios/Components/BorderedButton.swift diff --git a/ios/ios/Ui/Components/ColorScheme.swift b/ios/ios/Components/ColorScheme.swift similarity index 100% rename from ios/ios/Ui/Components/ColorScheme.swift rename to ios/ios/Components/ColorScheme.swift diff --git a/ios/ios/Ui/Components/DetailScreenHelperView.swift b/ios/ios/Components/DetailScreenHelperView.swift similarity index 100% rename from ios/ios/Ui/Components/DetailScreenHelperView.swift rename to ios/ios/Components/DetailScreenHelperView.swift diff --git a/ios/ios/Components/EmptyUIView.swift b/ios/ios/Components/EmptyUIView.swift new file mode 100644 index 000000000..bc2b40f72 --- /dev/null +++ b/ios/ios/Components/EmptyUIView.swift @@ -0,0 +1,36 @@ +// +// EmptyView.swift +// tv-maniac +// +// Created by Thomas Kioko on 04.12.23. +// Copyright © 2023 orgName. All rights reserved. +// + +import SwiftUI + +struct EmptyUIView: View { + var body: some View { + VStack{ + Spacer() + + + Text("🚧") + .titleBoldFont(size: 73) + .font(.title3) + .padding(16) + + Text("Construction In progress!!") + .titleBoldFont(size: 34) + .font(.title3) + .frame(maxWidth: .infinity) + + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +#Preview { + EmptyUIView() +} diff --git a/ios/ios/Ui/Components/FullScreenView.swift b/ios/ios/Components/FullScreenView.swift similarity index 100% rename from ios/ios/Ui/Components/FullScreenView.swift rename to ios/ios/Components/FullScreenView.swift diff --git a/ios/ios/Ui/Components/LoadingIndicatorView.swift b/ios/ios/Components/LoadingIndicatorView.swift similarity index 100% rename from ios/ios/Ui/Components/LoadingIndicatorView.swift rename to ios/ios/Components/LoadingIndicatorView.swift diff --git a/ios/ios/Ui/Components/OffsetModifier.swift b/ios/ios/Components/OffsetModifier.swift similarity index 100% rename from ios/ios/Ui/Components/OffsetModifier.swift rename to ios/ios/Components/OffsetModifier.swift diff --git a/ios/ios/Ui/Components/PosterStyle.swift b/ios/ios/Components/PosterStyle.swift similarity index 100% rename from ios/ios/Ui/Components/PosterStyle.swift rename to ios/ios/Components/PosterStyle.swift diff --git a/ios/ios/Ui/Components/ShowItem.swift b/ios/ios/Components/ShowItem.swift similarity index 100% rename from ios/ios/Ui/Components/ShowItem.swift rename to ios/ios/Components/ShowItem.swift diff --git a/ios/ios/Ui/Components/ShowPosterImage.swift b/ios/ios/Components/ShowPosterImage.swift similarity index 68% rename from ios/ios/Ui/Components/ShowPosterImage.swift rename to ios/ios/Components/ShowPosterImage.swift index e31a4eb25..772b8a0f5 100644 --- a/ios/ios/Ui/Components/ShowPosterImage.swift +++ b/ios/ios/Components/ShowPosterImage.swift @@ -5,13 +5,13 @@ struct ShowPosterImage: View { @Namespace var animation @State private var show: Bool = false - @State private var selectedShow: Int64 = -1 let posterSize: PosterStyle.Size let imageUrl: String? let showTitle: String let showId: Int64 - + var onClick : () -> Void + var body: some View { @@ -19,7 +19,6 @@ struct ShowPosterImage: View { if let posterUrl = imageUrl { KFImage.url(URL(string: posterUrl)) - .resizable() .loadDiskFileSynchronously() .cacheMemoryOnly() .fade(duration: 0.25) @@ -34,25 +33,16 @@ struct ShowPosterImage: View { .frame(width: posterSize.width(), height: posterSize.height()) .posterStyle(loaded: false, size: posterSize) } + .resizable() + .setProcessor(ResizingImageProcessor(referenceSize: CGSize(width: posterSize.width() * scale, height: posterSize.height() * scale), mode: .aspectFit)) .aspectRatio(contentMode: .fill) .frame(width: posterSize.width(), height: posterSize.height()) .cornerRadius(5) .shadow(radius: 10) .matchedGeometryEffect(id: showId, in: animation) - .onTapGesture { - /// Adding Animation - withAnimation(.interactiveSpring(response: 0.6, dampingFraction: 0.8, blendDuration: 0.8)) { - selectedShow = showId - show.toggle() - } - } - .detailScreenCover(show: $show) { - /// Detail View - ShowDetailView(showId: $selectedShow, animationID: animation) - } + .onTapGesture { onClick()} } else { ZStack { - Text(showTitle) .padding(.trailing, 16) .padding(.leading, 16) @@ -62,7 +52,7 @@ struct ShowPosterImage: View { .foregroundColor(Color.text_color_bg) .frame(width: posterSize.width(), height: posterSize.height()) .cornerRadius(10) - + Rectangle() .foregroundColor(Color.accent) @@ -71,18 +61,12 @@ struct ShowPosterImage: View { .cornerRadius(5) .shadow(radius: 10) .matchedGeometryEffect(id: showId, in: animation) - .onTapGesture { - /// Adding Animation - withAnimation(.interactiveSpring(response: 0.6, dampingFraction: 0.8, blendDuration: 0.8)) { - selectedShow = showId - show.toggle() - } - } - .detailScreenCover(show: $show) { - /// Detail View - ShowDetailView(showId: $selectedShow, animationID: animation) - } + .onTapGesture { onClick()} } } } + + private var scale: CGFloat { + UIScreen.main.scale + } } diff --git a/ios/ios/Ui/Components/ShowRow.swift b/ios/ios/Components/ShowRow.swift similarity index 90% rename from ios/ios/Ui/Components/ShowRow.swift rename to ios/ios/Components/ShowRow.swift index ae33a063b..d1b8fa784 100644 --- a/ios/ios/Ui/Components/ShowRow.swift +++ b/ios/ios/Components/ShowRow.swift @@ -6,7 +6,8 @@ struct ShowRow: View { @Namespace var animation var categoryName: String var shows: [TvShow]? - + var onClick : (Int64) -> Void + var body: some View { VStack(alignment: .leading) { if(shows?.isEmpty != true){ @@ -39,7 +40,8 @@ struct ShowRow: View { posterSize: .medium, imageUrl: item.posterImageUrl, showTitle: item.title, - showId: item.traktId + showId: item.traktId, + onClick: { onClick(item.traktId) } ) } } @@ -55,6 +57,6 @@ struct ShowRow: View { struct ShowRow_Previews: PreviewProvider { static var previews: some View { - ShowRow(categoryName: "Trending", shows: [mockTvShow,mockTvShow,mockTvShow]) + ShowRow(categoryName: "Trending", shows: [mockTvShow,mockTvShow,mockTvShow], onClick: { _ in }) } } diff --git a/ios/ios/Components/SnapCarousel.swift b/ios/ios/Components/SnapCarousel.swift new file mode 100644 index 000000000..4e6a0bf9b --- /dev/null +++ b/ios/ios/Components/SnapCarousel.swift @@ -0,0 +1,107 @@ +// +// SnapCarousel.swift +// tv-maniac +// +// Created by Thomas Kioko on 16.01.22. +// Copyright © 2022 orgName. All rights reserved. +// + +import SwiftUI +import TvManiac + +// See my Custom Snap Carousel Video... +// Link in Description... + +// To for acepting List.... +struct SnapCarousel: View { + + var content: (T) -> Content + var list: [T] + + // Properties.... + var spacing: CGFloat + var trailingSpace: CGFloat + @Binding var index: Int + + init(spacing: CGFloat = 15, trailingSpace: CGFloat = 100, index: Binding, items: [T], @ViewBuilder content: @escaping (T)->Content){ + + self.list = items + self.spacing = spacing + self.trailingSpace = trailingSpace + self._index = index + self.content = content + } + + // Offset... + @GestureState var offset: CGFloat = 0 + @State var currentIndex: Int = 2 + + var body: some View { + GeometryReader{proxy in + + // Settings correct Width for snap Carousel... + + // One Sided Snap Carousel + let width = proxy.size.width - ( trailingSpace - spacing ) + let adjustMentWidth = (trailingSpace / 2) - spacing + + HStack (spacing: spacing) { + ForEach(list, id: \.traktId) { item in + content(item) + .frame(width: proxy.size.width - trailingSpace) + } + + } + + // Spacing will be horizontal padding... + .padding(.horizontal, spacing) + // Setting only after 0th index... + .offset(x: (CGFloat(currentIndex) * -width) + ( currentIndex != 0 ? adjustMentWidth : 0 ) + offset) + .gesture( + DragGesture() + .updating($offset, body: { value, out, _ in + out = value.translation.width + }) + .onEnded({ value in + + // Updating Current Index.... + let offsetX = value.translation.width + + // Were going to convert the tranlsation into progreess ( 0 - 1 ) + // and round the value... + // based on the progress increasing or decreasing the currentInde.... + + let progress = -offsetX / width + let roundIndex = progress.rounded() + + // setting max.... + currentIndex = max(min(currentIndex + Int(roundIndex), list.count - 1), 0) + + // updating index.... + currentIndex = index + }) + .onChanged({ value in + // updating only index... + + // Updating Current Index.... + let offsetX = value.translation.width + + // Were going to convert the tranlsation into progreess ( 0 - 1 ) + // and round the value... + // based on the progress increasing or decreasing the currentInde.... + + let progress = -offsetX / width + let roundIndex = progress.rounded() + + // setting max.... + index = max(min(currentIndex + Int(roundIndex), list.count - 1), 0) + + }) + ) + + } + // Animatiing when offset = 0 + .animation(.easeInOut, value: offset == 0) + + } +} diff --git a/ios/ios/Ui/Components/TextViews.swift b/ios/ios/Components/TextViews.swift similarity index 100% rename from ios/ios/Ui/Components/TextViews.swift rename to ios/ios/Components/TextViews.swift diff --git a/ios/ios/Ui/Components/TintOverlay.swift b/ios/ios/Components/TintOverlay.swift similarity index 100% rename from ios/ios/Ui/Components/TintOverlay.swift rename to ios/ios/Components/TintOverlay.swift diff --git a/ios/ios/Ui/Components/Toast/Toast.swift b/ios/ios/Components/Toast/Toast.swift similarity index 100% rename from ios/ios/Ui/Components/Toast/Toast.swift rename to ios/ios/Components/Toast/Toast.swift diff --git a/ios/ios/Ui/Components/Toast/ToastModifier.swift b/ios/ios/Components/Toast/ToastModifier.swift similarity index 100% rename from ios/ios/Ui/Components/Toast/ToastModifier.swift rename to ios/ios/Components/Toast/ToastModifier.swift diff --git a/ios/ios/Ui/Components/Toast/ToastStyle.swift b/ios/ios/Components/Toast/ToastStyle.swift similarity index 100% rename from ios/ios/Ui/Components/Toast/ToastStyle.swift rename to ios/ios/Components/Toast/ToastStyle.swift diff --git a/ios/ios/Ui/Components/Toast/ToastView.swift b/ios/ios/Components/Toast/ToastView.swift similarity index 100% rename from ios/ios/Ui/Components/Toast/ToastView.swift rename to ios/ios/Components/Toast/ToastView.swift diff --git a/ios/ios/Discover/DiscoverView.swift b/ios/ios/Discover/DiscoverView.swift new file mode 100644 index 000000000..0db8588c6 --- /dev/null +++ b/ios/ios/Discover/DiscoverView.swift @@ -0,0 +1,192 @@ +import SwiftUI +import TvManiac +import os.log + +struct DiscoverView: View { + + @Environment(\.colorScheme) var scheme + + @State var currentIndex: Int = 2 + + private let presenter: DiscoverShowsPresenter + + @StateValue + private var uiState: DiscoverState + + init(presenter: DiscoverShowsPresenter){ + self.presenter = presenter + _uiState = StateValue(presenter.state) + } + + var body: some View { + NavigationStack { + VStack { + switch uiState { + case is Loading: + LoadingIndicatorView() + .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .center) + case is DataLoaded: DiscoverContent(presenter: presenter) + default: + fatalError("Unhandled case: \(uiState)") + } + } + .background(Color.background) + .toolbar {} + .navigationTitle("") + } + } + + + @ViewBuilder + func DiscoverContent(presenter: DiscoverShowsPresenter) -> some View { + ZStack { + let contentState = uiState as! DataLoaded + + BackgroundView(tvShows: contentState.recommendedShows) + + ScrollView(.vertical, showsIndicators: false) { + VStack { + let state = contentState + + if(state.errorMessage != nil) { + FullScreenView(systemName: "exclamationmark.triangle", message: state.errorMessage!) + } else { + + //Featured Shows + FeaturedContentView(tvShows: state.recommendedShows) + + //Anticipated shows + ShowRow( + categoryName: "Anticipated", + shows: state.anticipatedShows, + onClick: { id in + presenter.dispatch(action: ShowClicked(id: id)) + } + ) + + //Trending shows + ShowRow( + categoryName: "Trending", + shows: state.trendingShows, + onClick: { id in + presenter.dispatch(action: ShowClicked(id: id)) + } + ) + + //Popular Shows + ShowRow( + categoryName: "Popular", + shows: state.popularShows, + onClick: { id in + presenter.dispatch(action: ShowClicked(id: id)) + } + ) + + + } + + } + } + + } + .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .center) + + } + + + @ViewBuilder + func FeaturedContentView(tvShows: [TvShow]?) -> some View { + if let shows = tvShows { + if !shows.isEmpty { + SnapCarousel(spacing: 10, trailingSpace: 120,index: $currentIndex, items: shows) { post in + + GeometryReader{ proxy in + + ShowPosterImage( + posterSize: .big, + imageUrl: post.posterImageUrl, + showTitle: post.title, + showId: post.traktId, + onClick: { presenter.dispatch(action: ShowClicked(id: post.traktId)) } + ) + .cornerRadius(12) + .shadow(color: Color("shadow1"), radius: 4, x: 0, y: 4) + .transition(AnyTransition.slide) + } + } + .edgesIgnoringSafeArea(.all) + .frame(height: 450) + .padding(.top, 70) + + + CustomIndicator(shows: shows) + .padding() + .padding(.top, 10) + } + } + } + + + + + + @ViewBuilder + func BackgroundView(tvShows: [TvShow]?) -> some View { + if let shows = tvShows { + if !shows.isEmpty { + GeometryReader { proxy in + let size = proxy.size + + TabView(selection: $currentIndex) { + ForEach(shows.indices, id: \.self) { index in + ShowPosterImage( + posterSize: .big, + imageUrl: shows[index].posterImageUrl, + showTitle: shows[index].title, + showId: shows[index].traktId, + onClick: { } + ) + .aspectRatio(contentMode: .fill) + .frame(width: size.width + 350, height: size.height + 200) + .tag(index) + } + + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .animation(.easeInOut, value: currentIndex) + + + let color: Color = (scheme == .dark ? .black : .white) + // Custom Gradient + LinearGradient(colors: [ + .black, + .clear, + color.opacity(0.15), + color.opacity(0.5), + color.opacity(0.8), + color, + color + ], startPoint: .top, endPoint: .bottom) + + // Blurred Overlay + Rectangle() + .fill(.ultraThinMaterial) + } + .ignoresSafeArea() + } + } + } + + @ViewBuilder + func CustomIndicator(shows: [TvShow]) -> some View { + HStack(spacing: 5) { + ForEach(shows.indices, id: \.self) { index in + Circle() + .fill(currentIndex == index ? Color.accent_color : .gray.opacity(0.5)) + .frame(width: currentIndex == index ? 10 : 6, height: currentIndex == index ? 10 : 6) + } + } + .animation(.easeInOut, value: currentIndex) + } + +} diff --git a/ios/ios/Ui/Extensions/ColorExtension.swift b/ios/ios/Extensions/ColorExtension.swift similarity index 94% rename from ios/ios/Ui/Extensions/ColorExtension.swift rename to ios/ios/Extensions/ColorExtension.swift index f0471e819..cff530e66 100644 --- a/ios/ios/Ui/Extensions/ColorExtension.swift +++ b/ios/ios/Extensions/ColorExtension.swift @@ -64,5 +64,8 @@ extension Color { Color("Background", bundle: nil) } + public static var blue: Color { + Color("blue", bundle: nil) + } } diff --git a/ios/ios/Ui/Extensions/Font.swift b/ios/ios/Extensions/Font.swift similarity index 100% rename from ios/ios/Ui/Extensions/Font.swift rename to ios/ios/Extensions/Font.swift diff --git a/ios/ios/Ui/Font/FjallaOne-Regular.ttf b/ios/ios/Extensions/Font/FjallaOne-Regular.ttf similarity index 100% rename from ios/ios/Ui/Font/FjallaOne-Regular.ttf rename to ios/ios/Extensions/Font/FjallaOne-Regular.ttf diff --git a/ios/ios/Ui/Font/WorkSans-Black.ttf b/ios/ios/Extensions/Font/WorkSans-Black.ttf similarity index 100% rename from ios/ios/Ui/Font/WorkSans-Black.ttf rename to ios/ios/Extensions/Font/WorkSans-Black.ttf diff --git a/ios/ios/Ui/Font/WorkSans-Bold.ttf b/ios/ios/Extensions/Font/WorkSans-Bold.ttf similarity index 100% rename from ios/ios/Ui/Font/WorkSans-Bold.ttf rename to ios/ios/Extensions/Font/WorkSans-Bold.ttf diff --git a/ios/ios/Ui/Font/WorkSans-ExtraBold.ttf b/ios/ios/Extensions/Font/WorkSans-ExtraBold.ttf similarity index 100% rename from ios/ios/Ui/Font/WorkSans-ExtraBold.ttf rename to ios/ios/Extensions/Font/WorkSans-ExtraBold.ttf diff --git a/ios/ios/Ui/Font/WorkSans-ExtraLight.ttf b/ios/ios/Extensions/Font/WorkSans-ExtraLight.ttf similarity index 100% rename from ios/ios/Ui/Font/WorkSans-ExtraLight.ttf rename to ios/ios/Extensions/Font/WorkSans-ExtraLight.ttf diff --git a/ios/ios/Ui/Font/WorkSans-Light.ttf b/ios/ios/Extensions/Font/WorkSans-Light.ttf similarity index 100% rename from ios/ios/Ui/Font/WorkSans-Light.ttf rename to ios/ios/Extensions/Font/WorkSans-Light.ttf diff --git a/ios/ios/Ui/Font/WorkSans-Medium.ttf b/ios/ios/Extensions/Font/WorkSans-Medium.ttf similarity index 100% rename from ios/ios/Ui/Font/WorkSans-Medium.ttf rename to ios/ios/Extensions/Font/WorkSans-Medium.ttf diff --git a/ios/ios/Ui/Font/WorkSans-Regular.ttf b/ios/ios/Extensions/Font/WorkSans-Regular.ttf similarity index 100% rename from ios/ios/Ui/Font/WorkSans-Regular.ttf rename to ios/ios/Extensions/Font/WorkSans-Regular.ttf diff --git a/ios/ios/Ui/Font/WorkSans-SemiBold.ttf b/ios/ios/Extensions/Font/WorkSans-SemiBold.ttf similarity index 100% rename from ios/ios/Ui/Font/WorkSans-SemiBold.ttf rename to ios/ios/Extensions/Font/WorkSans-SemiBold.ttf diff --git a/ios/ios/Ui/Font/WorkSans-Thin.ttf b/ios/ios/Extensions/Font/WorkSans-Thin.ttf similarity index 100% rename from ios/ios/Ui/Font/WorkSans-Thin.ttf rename to ios/ios/Extensions/Font/WorkSans-Thin.ttf diff --git a/ios/ios/Ui/Extensions/OverlayExtension.swift b/ios/ios/Extensions/OverlayExtension.swift similarity index 100% rename from ios/ios/Ui/Extensions/OverlayExtension.swift rename to ios/ios/Extensions/OverlayExtension.swift diff --git a/ios/ios/Ui/Extensions/PrintExtension.swift b/ios/ios/Extensions/PrintExtension.swift similarity index 100% rename from ios/ios/Ui/Extensions/PrintExtension.swift rename to ios/ios/Extensions/PrintExtension.swift diff --git a/ios/ios/Feature/Detail/ShowBodyView.swift b/ios/ios/Feature/Detail/ShowBodyView.swift index 7230822f1..219ff2157 100644 --- a/ios/ios/Feature/Detail/ShowBodyView.swift +++ b/ios/ios/Feature/Detail/ShowBodyView.swift @@ -20,6 +20,7 @@ struct ShowBodyView: View { var seasonList: [Season] var trailerList: [Trailer] var similarShowsList: [Show] + var onClick : (Int64) -> Void var body: some View { @@ -69,7 +70,8 @@ struct ShowBodyView: View { posterSize: .medium, imageUrl: item.posterImageUrl, showTitle: item.title, - showId: item.traktId + showId: item.traktId, + onClick: { onClick(item.traktId)} ) } @@ -94,7 +96,8 @@ struct ShowBodyView_Previews: PreviewProvider { ShowBodyView( seasonList: detailState.seasonsContent.seasonsList, trailerList: detailState.trailersContent.trailersList, - similarShowsList: detailState.similarShowsContent.similarShows + similarShowsList: detailState.similarShowsContent.similarShows, + onClick: { _ in } ) } } diff --git a/ios/ios/Feature/Detail/ShowDetailView.swift b/ios/ios/Feature/Detail/ShowDetailView.swift index 3bb183d85..ae0e741ca 100644 --- a/ios/ios/Feature/Detail/ShowDetailView.swift +++ b/ios/ios/Feature/Detail/ShowDetailView.swift @@ -11,66 +11,51 @@ import TvManiac struct ShowDetailView: View { - @Binding var showId: Int64 - var animationID: Namespace.ID + private let presenter: ShowDetailsPresenter - @ObservedObject var viewModel: ShowDetailsViewModel = ShowDetailsViewModel() + @StateValue + private var uiState: ShowDetailsState + // var animationID: Namespace.ID @State var offset: CGFloat = 0 @State var titleOffset: CGFloat = 0 @State var size: CGSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) - + let maxHeight = CGFloat(520) + init(presenter: ShowDetailsPresenter){ + self.presenter = presenter + _uiState = StateValue(presenter.state) + } + var body: some View { VStack { - - switch viewModel.detailState { - - case is ShowDetailsLoaded: - let state = viewModel.detailState as! ShowDetailsLoaded - - if(state.isLoading){ - LoadingIndicatorView() - .frame(maxWidth: size.width, maxHeight: size.height, alignment: .center) - } else if(state.errorMessage != nil){ - //TODO:: Show Toast - } else { - ScrollView(.vertical, showsIndicators: false) { - VStack { - ArtWork(show: state.show) - - ShowBodyView( - seasonList: state.seasonsContent.seasonsList, - trailerList: state.trailersContent.trailersList, - similarShowsList: state.similarShowsContent.similarShows - ) - } - } - .coordinateSpace(name: "SCROLL") + ScrollView(.vertical, showsIndicators: false) { + VStack { + + ArtWork(show: uiState.show, presenter: presenter) + + ShowBodyView( + seasonList: uiState.seasonsContent.seasonsList, + trailerList: uiState.trailersContent.trailersList, + similarShowsList: uiState.similarShowsContent.similarShows, + onClick: { id in presenter.dispatch(action: DetailShowClicked(id: id))} + ) } - - default: - let _ = print("Unhandled case: \(viewModel.detailState)") - FullScreenView(systemName: "exclamationmark.triangle", message: "Something went wrong") } + .coordinateSpace(name: "SCROLL") } .overlay(alignment: .top){ - if let state = viewModel.detailState as? ShowDetailsLoaded { - TopNavBarView(showTitle: state.show.title) - } else { - TopNavBarView(showTitle: "") - } + TopNavBarView(showTitle: uiState.show.title) } .background(Color.background) .navigationBarHidden(true) .ignoresSafeArea() - .onAppear { viewModel.startStateMachine(showId: showId) } } @ViewBuilder - func ArtWork(show: Show) -> some View { + func ArtWork(show: Show, presenter: ShowDetailsPresenter) -> some View { let height = size.height * 0.45 GeometryReader { proxy in @@ -82,10 +67,12 @@ struct ShowDetailView: View { posterSize: .max, imageUrl: show.backdropImageUrl, showTitle: show.title, - showId: show.traktId + showId: show.traktId, + onClick: { presenter.dispatch(action: DetailShowClicked(id: show.traktId))} ) .aspectRatio(contentMode: .fill) .frame(width: size.width, height: size.height + (minY > 0 ? minY : 0)) + .foregroundStyle(.ultraThinMaterial) .clipped() .overlay( content : { ZStack(alignment: .bottom) { @@ -105,7 +92,7 @@ struct ShowDetailView: View { ) //Header Content - HeaderContentView(show: show) + HeaderContentView(show: show, presenter: presenter) .opacity(1 + (progress > 0 ? -progress : progress)) .padding(.horizontal,16) // Moving With ScrollView @@ -128,7 +115,8 @@ struct ShowDetailView: View { TopNavBar( titleProgress: titleProgress, - title: showTitle + title: showTitle, + action: { presenter.dispatch(action: DetailBackClicked())} ) .padding(.top, 45) .padding([.horizontal,],15) @@ -141,7 +129,7 @@ struct ShowDetailView: View { } @ViewBuilder - func HeaderContentView(show: Show) -> some View { + func HeaderContentView(show: Show, presenter: ShowDetailsPresenter) -> some View { VStack(spacing: 0){ Text(show.title) @@ -170,19 +158,19 @@ struct ShowDetailView: View { color: .accent, borderColor: .grey_200, isOn: false, - action: { - //TODO:: Navigate to trailer view - }) + action: { presenter.dispatch(action: WatchTrailerClicked(id: show.traktId)) } + ) + + let followText = if (!show.isFollowed) { "Follow Show" } else { "Unfollow Show"} + let buttonSystemImage = if (!show.isFollowed) { "plus.square.fill.on.square.fill" } else { "checkmark.square.fill"} BorderedButton( - text: "Follow Show", - systemImageName: "plus.app.fill", + text: followText, + systemImageName: buttonSystemImage, color: .accent, borderColor: .grey_200, isOn: false, - action: { - viewModel.dispatchAction(showId: showId, action: FollowShowClicked(addToFollowed: show.isFollowed)) - } + action: { presenter.dispatch(action: FollowShowClicked(addToLibrary: show.isFollowed)) } ) } .padding(.bottom, 16) diff --git a/ios/ios/Feature/Detail/ShowDetailsViewModel.swift b/ios/ios/Feature/Detail/ShowDetailsViewModel.swift deleted file mode 100644 index 2140997e1..000000000 --- a/ios/ios/Feature/Detail/ShowDetailsViewModel.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Created by Thomas Kioko on 24.11.22. -// Copyright (c) 2022 orgName. All rights reserved. -// - -import Foundation -import TvManiac - -class ShowDetailsViewModel: ObservableObject { - private let stateMachine: ShowDetailsStateMachineWrapper = ApplicationComponentKt.showDetailsStateMachine() - - @Published private(set) var detailState: ShowDetailsState = ShowDetailsLoaded.companion.EMPTY_DETAIL_STATE - - func startStateMachine(showId: Int64) { - stateMachine.start(showId: showId, stateChangeListener: { (state: ShowDetailsState) -> Void in - self.detailState = state - }) - } - - func dispatchAction(showId: Int64, action: ShowDetailsAction){ - stateMachine.dispatch(showId: showId, action: action) - } - -} diff --git a/ios/ios/Feature/Detail/TopNavBar.swift b/ios/ios/Feature/Detail/TopNavBar.swift index ee7500f2c..b8b042015 100644 --- a/ios/ios/Feature/Detail/TopNavBar.swift +++ b/ios/ios/Feature/Detail/TopNavBar.swift @@ -10,6 +10,7 @@ struct TopNavBar: View { var titleProgress: CGFloat var title: String + let action: () -> Void @Environment(\.presentationMode) var presentationMode: Binding @@ -17,14 +18,16 @@ struct TopNavBar: View { HStack(spacing: 15) { - Button { - presentationMode.wrappedValue.dismiss() - } label: { - Image(systemName: "chevron.left") + Button(action: action) { + Image(systemName: "arrow.backward.circle.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 32, height: 34) .font(.body.bold()) .foregroundColor(.white) .padding([.top, .bottom,.trailing]) } + .symbolVariant(.circle.fill) Spacer(minLength: 0) diff --git a/ios/ios/Feature/Discover/DiscoverShowsViewmodel.swift b/ios/ios/Feature/Discover/DiscoverShowsViewmodel.swift deleted file mode 100644 index f9581f983..000000000 --- a/ios/ios/Feature/Discover/DiscoverShowsViewmodel.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// DiscoverShowsViewmodel.swift -// tv-maniac -// -// Created by Thomas Kioko on 06.11.22. -// Copyright © 2022 orgName. All rights reserved. -// - -import Foundation -import TvManiac - - -class DiscoverShowsViewModel: ObservableObject { - private let stateMachine: DiscoverStateMachineWrapper = ApplicationComponentKt.discoverStateMachine() - @Published private(set) var showState: DiscoverState = Loading() - @Published var toast: Toast? = nil - - func startStateMachine() { - stateMachine.start(stateChangeListener: { (state: DiscoverState) -> Void in - self.showState = state - if(state is DataLoaded){ - let dataLoaded = state as! DataLoaded - if(!dataLoaded.isContentEmpty && dataLoaded.errorMessage != nil){ - self.toast = Toast(type: .error, title: "Error", message: dataLoaded.errorMessage!) - } - } - }) - } -} diff --git a/ios/ios/Feature/Discover/DiscoverView.swift b/ios/ios/Feature/Discover/DiscoverView.swift deleted file mode 100644 index e0b2103b9..000000000 --- a/ios/ios/Feature/Discover/DiscoverView.swift +++ /dev/null @@ -1,191 +0,0 @@ -import SwiftUI -import TvManiac -import os.log - -struct DiscoverView: View { - - @StateObject private var viewModel: DiscoverShowsViewModel = DiscoverShowsViewModel() - - @Environment(\.colorScheme) var scheme - - @State var currentIndex: Int = 2 - @State private var show: Bool = false - @State private var showId: Int64 = -1 - @State private var regularSheet: Bool = false - - var body: some View { - VStack { - switch viewModel.showState { - case is Loading: - LoadingIndicatorView() - .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .center) - case is DataLoaded: - let state = viewModel.showState as! DataLoaded - DiscoverContent(contentState: state) - default: - fatalError("Unhandled case: \(viewModel.showState)") - } - } - .background(Color.background) - .onAppear { viewModel.startStateMachine() } } - - - @ViewBuilder - func DiscoverContent(contentState: DiscoverState) -> some View { - NavigationStack { - - ZStack { - BackgroundView(contentState: contentState) - - GeometryReader { geometry in - ScrollView(.vertical, showsIndicators: false) { - VStack { - switch contentState { - case is DataLoaded: - let state = contentState as! DataLoaded - - if(state.isContentEmpty && state.errorMessage != nil) { - FullScreenView(systemName: "exclamationmark.triangle", message: state.errorMessage!) - } else if(state.isContentEmpty){ - FullScreenView(systemName: "list.and.film", message: "Looks like your stash is empty") - } else { - - //Featured Shows - FeaturedContentView(tvShows: state.recommendedShows) - - //Anticipated shows - ShowRow( - categoryName: "Anticipated", - shows: state.anticipatedShows - ) - - //Trending shows - ShowRow( - categoryName: "Trending", - shows: state.trendingShows - ) - - //Popular Shows - ShowRow( - categoryName: "Popular", - shows: state.popularShows - ) - - Spacer() - } - default: - let _ = print("Unhandled case: \(contentState)") - } - } - .frame(width: geometry.size.width) - .frame(minHeight: geometry.size.height) - } - } - } - }.toastView(toast: $viewModel.toast) - } - - - @ViewBuilder - func FeaturedContentView(tvShows: [TvShow]?) -> some View { - if let shows = tvShows { - if !shows.isEmpty { - SnapCarousel(spacing: 10, trailingSpace: 70, index: $currentIndex, items: shows) { item in - - GeometryReader { proxy in - let size = proxy.size - - ShowPosterImage( - posterSize: .big, - imageUrl: item.posterImageUrl, - showTitle: item.title, - showId: item.traktId - ) - .frame(width: size.width, height: size.height) - } - } - .frame(height: 450) - .padding(.top, 90) - - CustomIndicator(shows: shows) - } - } - } - - @ViewBuilder - func BackgroundView(contentState: DiscoverState) -> some View { - if contentState is DataLoaded { - let state = contentState as! DataLoaded - - if let tvShows = state.recommendedShows { - if !tvShows.isEmpty { - GeometryReader { proxy in - let size = proxy.size - - TabView(selection: $currentIndex) { - ForEach(tvShows.indices, id: \.self) { index in - ShowPosterImage( - posterSize: .big, - imageUrl: tvShows[index].posterImageUrl, - showTitle: tvShows[index].title, - showId: tvShows[index].traktId - ) - .aspectRatio(contentMode: .fill) - .frame(width: size.width, height: size.height) - - .tag(index) - } - - } - .tabViewStyle(.page(indexDisplayMode: .never)) - .animation(.easeInOut, value: currentIndex) - - - let color: Color = (scheme == .dark ? .black : .white) - // Custom Gradient - LinearGradient(colors: [ - .black, - .clear, - color.opacity(0.15), - color.opacity(0.5), - color.opacity(0.8), - color, - color - ], startPoint: .top, endPoint: .bottom) - - // Blurred Overlay - Rectangle() - .fill(.ultraThinMaterial) - } - .ignoresSafeArea() - } - } - } - } - - @ViewBuilder - func CustomIndicator(shows: [TvShow]) -> some View { - HStack(spacing: 5) { - ForEach(shows.indices, id: \.self) { index in - Circle() - .fill(currentIndex == index ? Color.accent_color : .gray.opacity(0.5)) - .frame(width: currentIndex == index ? 10 : 6, height: currentIndex == index ? 10 : 6) - } - - - - - } - .animation(.easeInOut, value: currentIndex) - } - -} - -struct DiscoverView_Previews: PreviewProvider { - static var previews: some View { - DiscoverView() - - DiscoverView() - .preferredColorScheme(.dark) - } -} diff --git a/ios/ios/Feature/Discover/DiscoverViewModel.swift b/ios/ios/Feature/Discover/DiscoverViewModel.swift deleted file mode 100644 index 2d06bc2c1..000000000 --- a/ios/ios/Feature/Discover/DiscoverViewModel.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// DiscoverVIewModel.swift -// tv-maniac -// -// Created by Thomas Kioko on 31.10.21. -// Copyright © 2021 orgName. All rights reserved. -// - -import Foundation -import Combine -import TvManiac - -final class DiscoverViewModel: BaseViewModel, ObservableObject { - - @Published var state: DiscoverShowState = DiscoverShowState.companion.Empty - - private let logger = Logger(className: "DiscoverViewModel") - - private var showsCancellable: AnyCancellable? - - private let interactor: ObserveDiscoverShowsInteractor - - init(interactor: ObserveDiscoverShowsInteractor) { - self.interactor = interactor - } - - func startObservingDiscoverShows() { - - interactor.execute(self, args: nil) { - $0.onNext { result in - self.updateState(showResult: result!) - } - - $0.onError { error in - self.logger.log(msg: "\(error)") - } - } - } - - - private func updateState( - showResult: DiscoverShowResult - ) { - - state = DiscoverShowState( - isLoading: showResult.trendingShows.isLoading, - showData: showResult - ) - - } - - - func stopObservingTrendingShows() { - showsCancellable?.cancel() - } - -} diff --git a/ios/ios/Feature/Home/HomeUIView.swift b/ios/ios/Feature/Home/HomeUIView.swift deleted file mode 100644 index d37d51a76..000000000 --- a/ios/ios/Feature/Home/HomeUIView.swift +++ /dev/null @@ -1,56 +0,0 @@ -import SwiftUI - - -struct HomeUIView: View { - - - var body: some View { - - TabView { - DiscoverView() - .setTabItem("Discover", "film") - .setTabBarBackground(.init(.ultraThickMaterial)) - - - SearchView() - .setTabItem("Search", "magnifyingglass") - .setTabBarBackground(.init(.ultraThickMaterial)) - - WatchlistView() - .setTabItem("Watchlist", "list.bullet.below.rectangle") - .setTabBarBackground(.init(.ultraThickMaterial)) - - - ProfileView() - .setTabItem("Profile", "person.circle") - .setTabBarBackground(.init(.ultraThickMaterial)) - - } - .tint(Color.accent_color) - } -} - -/// Custom View Modifier's -extension View { - @ViewBuilder - func setTabItem(_ title: String, _ icon: String) -> some View { - self - .tabItem { - Image(systemName: icon) - Text(title) - } - } - - @ViewBuilder - func setTabBarBackground(_ style: AnyShapeStyle) -> some View { - self - .toolbarBackground(.visible, for: .tabBar) - .toolbarBackground(style, for: .tabBar) - } - - @ViewBuilder - func hideTabBar(_ status: Bool) -> some View { - self - .toolbar(status ? .hidden : .visible, for: .tabBar) - } -} diff --git a/ios/ios/Feature/LibraryView.swift b/ios/ios/Feature/LibraryView.swift new file mode 100644 index 000000000..3bb17d0c4 --- /dev/null +++ b/ios/ios/Feature/LibraryView.swift @@ -0,0 +1,121 @@ +// +// WatchlistView.swift +// tv-maniac +// +// Created by Thomas Kioko on 19.08.21. +// Copyright © 2021 orgName. All rights reserved. +// + +import SwiftUI +import TvManiac + +struct LibraryView: View { + + private let presenter: LibraryPresenter + + @StateValue + private var uiState: LibraryState + + init(presenter: LibraryPresenter){ + self.presenter = presenter + _uiState = StateValue(presenter.state) + } + + var body: some View { + NavigationStack { + VStack { + switch uiState { + case is LoadingShows: + //TODO:: Show indicator on the toolbar + LoadingIndicatorView() + .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .center) + case is LibraryContent: GridViewContent() + case is ErrorLoadingShows: + //TODO:: Show Error + EmptyView() + default: + fatalError("Unhandled case: \(uiState)") + } + } + .navigationTitle("Library") + .navigationBarTitleDisplayMode(.large) + .background(Color.background) + .toolbar { + ToolbarItem(placement: .primaryAction) { + HStack { + filterButton + sortButton + } + .padding(.vertical, 4) + } + } + } + + } + + @ViewBuilder + private func GridViewContent() -> some View { + let state = uiState as! LibraryContent + if !state.list.isEmpty { + ScrollView(.vertical, showsIndicators: false) { + LazyVGrid(columns: DrawingConstants.posterColumns,spacing: 16){ + ForEach(state.list, id: \.traktId){ item in + ShowPosterImage( + posterSize: .medium, + imageUrl: item.posterImageUrl, + showTitle: item.title, + showId: item.traktId, + onClick: { presenter.dispatch(action: LibraryShowClicked(id: item.traktId)) } + ) + } + }.padding(.all, 10) + } + } else { + empty + } + } + + private var filterButton: some View { + Button { + withAnimation { + //TODO:: Show Filter menu + } + } label: { + Label("Sort List", systemImage: "line.3.horizontal.decrease") + .foregroundColor(.white) + .labelStyle(.iconOnly) + } + .buttonBorderShape(.roundedRectangle(radius: 16)) + .buttonStyle(.bordered) + } + + private var sortButton: some View { + Button { + //TODO:: Add filer option + } label: { + Label("Sort Order", systemImage: "arrow.up.arrow.down.circle") + .labelStyle(.iconOnly) + } + .pickerStyle(.navigationLink) + .buttonBorderShape(.roundedRectangle(radius: 16)) + .buttonStyle(.bordered) + } + + @ViewBuilder + private var empty: some View { + if #available(iOS 17, *), #available(watchOS 10, *), #available(tvOS 17, *), #available(macOS 14, *) { + ContentUnavailableView("Your list is empty.", systemImage: "rectangle.on.rectangle") + .padding() + } else { + Text("Your list is empty") + .multilineTextAlignment(.center) + .font(.callout) + .foregroundColor(.secondary) + } + } +} + +private struct DrawingConstants { + static let posterColumns = [GridItem(.adaptive(minimum: 100))] + static let spacing: CGFloat = 20 +} diff --git a/ios/ios/Feature/Profile/Authenticated/AuthenticatedProfileView.swift b/ios/ios/Feature/Profile/Authenticated/AuthenticatedProfileView.swift index 8450e481d..7d1721ca9 100644 --- a/ios/ios/Feature/Profile/Authenticated/AuthenticatedProfileView.swift +++ b/ios/ios/Feature/Profile/Authenticated/AuthenticatedProfileView.swift @@ -10,8 +10,6 @@ import SwiftUI struct AuthenticatedProfileView: View { - @ObservedObject private var model = ProfileViewModel() - var body: some View { ZStack { VStack { diff --git a/ios/ios/Feature/Profile/ProfileView.swift b/ios/ios/Feature/Profile/ProfileView.swift index db9c4a12a..73cf4e816 100644 --- a/ios/ios/Feature/Profile/ProfileView.swift +++ b/ios/ios/Feature/Profile/ProfileView.swift @@ -10,14 +10,15 @@ import SwiftUI struct ProfileView: View { - @ObservedObject private var model = ProfileViewModel() + @State var isPresented = false + @State var isAuthenticated = false var body: some View { NavigationView { - if(!model.isAuthenticated){ + if(!isAuthenticated){ UnauthentivatedProfileView() .toolbar{ ToolbarItem(placement: .navigationBarTrailing) { @@ -34,7 +35,9 @@ struct ProfileView: View { ) .sheet( isPresented: $isPresented, - content: { SettingsUIView() } + content: { + //SettingsUIView() + } ) } } diff --git a/ios/ios/Feature/Profile/ProfileViewModel.swift b/ios/ios/Feature/Profile/ProfileViewModel.swift deleted file mode 100644 index 9debf0c72..000000000 --- a/ios/ios/Feature/Profile/ProfileViewModel.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ProfileViewModel.swift -// tv-maniac -// -// Created by Kioko on 03/04/2023. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation - -class ProfileViewModel : ObservableObject { - - var isAuthenticated : Bool = false - -} diff --git a/ios/ios/Feature/Search/SearchView.swift b/ios/ios/Feature/Search/SearchView.swift deleted file mode 100644 index 057a36938..000000000 --- a/ios/ios/Feature/Search/SearchView.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// SearchView.swift -// tv-maniac -// -// Created by Thomas Kioko on 19.08.21. -// Copyright © 2021 orgName. All rights reserved. -// - -import SwiftUI - -struct SearchView: View { - var body: some View { - ZStack { - VStack { - Text("Search Shows") - - Spacer() - } - .frame(width : CGFloat(480.0)) - .background(Color("Background")) - - } - } -} - -struct SearchView_Previews: PreviewProvider { - static var previews: some View { - SearchView() - - SearchView() - .preferredColorScheme(.dark) - } -} diff --git a/ios/ios/Feature/Settings/SettingsViewModel.swift b/ios/ios/Feature/Settings/SettingsViewModel.swift deleted file mode 100644 index d4e1fb753..000000000 --- a/ios/ios/Feature/Settings/SettingsViewModel.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// SettingsViewModel.swift -// tv-maniac -// -// Created by Thomas Kioko on 07.12.22. -// Copyright © 2022 orgName. All rights reserved. -// - -import Foundation -import TvManiac - -class SettingsViewModel: ObservableObject { - - private var stateMachine: SettingsStateMachineWrapper = ApplicationComponentKt.settingsStateMachine() - @Published private (set) var settingsState: SettingsState = Default.companion.EMPTY - @Published var appTheme: AppTheme = AppTheme.System - - func startStateMachine() { - stateMachine.start(stateChangeListener: { (state: SettingsState) -> Void in - self.settingsState = state - - if let themeState = state as? Default { - self.appTheme = toAppTheme(theme: themeState.theme) - } - }) - } - - func dispatchAction(action: SettingsActions){ - stateMachine.dispatch(action: action) - } - - func cancel() { - stateMachine.cancel() - } -} diff --git a/ios/ios/Feature/WatchlistView.swift b/ios/ios/Feature/WatchlistView.swift deleted file mode 100644 index 85161ed39..000000000 --- a/ios/ios/Feature/WatchlistView.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// WatchlistView.swift -// tv-maniac -// -// Created by Thomas Kioko on 19.08.21. -// Copyright © 2021 orgName. All rights reserved. -// - -import SwiftUI - -struct WatchlistView: View { - var body: some View { - ZStack { - VStack { - Text("Watchlist") - - Spacer() - } - .frame(width : CGFloat(480.0)) - .background(Color("Background")) - } - } -} - -struct WatchlistView_Previews: PreviewProvider { - static var previews: some View { - WatchlistView() - - WatchlistView() - .preferredColorScheme(.dark) - } -} diff --git a/ios/ios/Featured/FeaturedView.swift b/ios/ios/Featured/FeaturedView.swift new file mode 100644 index 000000000..b131a517a --- /dev/null +++ b/ios/ios/Featured/FeaturedView.swift @@ -0,0 +1,118 @@ +// +// FeaturedView.swift +// tv-maniac +// +// Created by Thomas Kioko on 09.12.23. +// Copyright © 2023 orgName. All rights reserved. +// + +import SwiftUI +import TvManiac + +struct FeaturedView: View { + + @Environment(\.colorScheme) var scheme + @State var currentIndex: Int = 2 + + var tvShows: [TvShow]? + var onClick : (Int64) -> Void + + var body: some View { + + ZStack { + BackgroundView(tvShows: tvShows) + + if let shows = tvShows { + if !shows.isEmpty { + SnapCarousel(spacing: 10, trailingSpace: 120,index: $currentIndex, items: shows) { show in + + GeometryReader{ proxy in + + let size = proxy.size + + ShowPosterImage( + posterSize: .big, + imageUrl: show.posterImageUrl, + showTitle: show.title, + showId: show.traktId, + onClick: { onClick(show.traktId) } + ) + .frame(width: size.width, height: size.height) + .cornerRadius(12) + .shadow(color: Color("shadow1"), radius: 4, x: 0, y: 4) + .transition(AnyTransition.slide) + .animation(.spring()) + } + } + + + + CustomIndicator(shows: shows) + .padding() + .padding(.top, 10) + } + } + } .frame(height: 450) + .padding(.top, 80) + } + + @ViewBuilder + func CustomIndicator(shows: [TvShow]) -> some View { + HStack(spacing: 5) { + ForEach(shows.indices, id: \.self) { index in + Circle() + .fill(currentIndex == index ? Color.accent_color : .gray.opacity(0.5)) + .frame(width: currentIndex == index ? 10 : 6, height: currentIndex == index ? 10 : 6) + } + } + .animation(.easeInOut, value: currentIndex) + } + + + @ViewBuilder + func BackgroundView(tvShows: [TvShow]?) -> some View { + if let shows = tvShows { + if !shows.isEmpty { + GeometryReader { proxy in + let size = proxy.size + + TabView(selection: $currentIndex) { + ForEach(shows.indices, id: \.self) { index in + ShowPosterImage( + posterSize: .big, + imageUrl: shows[index].posterImageUrl, + showTitle: shows[index].title, + showId: shows[index].traktId, + onClick: { } + ) + .aspectRatio(contentMode: .fill) + .frame(width: size.width + 250, height: size.height + 200) + .tag(index) + } + + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .animation(.easeInOut, value: currentIndex) + + + let color: Color = (scheme == .dark ? .black : .white) + // Custom Gradient + LinearGradient(colors: [ + .black, + .clear, + color.opacity(0.15), + color.opacity(0.5), + color.opacity(0.8), + color, + color + ], startPoint: .top, endPoint: .bottom) + + // Blurred Overlay + Rectangle() + .fill(.ultraThinMaterial) + } + .ignoresSafeArea() + } + } + } +} diff --git a/ios/ios/ObservableValue.swift b/ios/ios/ObservableValue.swift new file mode 100644 index 000000000..b485ae5a1 --- /dev/null +++ b/ios/ios/ObservableValue.swift @@ -0,0 +1,26 @@ +// +// ObservableValue.swift +// tv-maniac +// +// Created by Thomas Kioko on 03.12.23. +// Copyright © 2023 orgName. All rights reserved. +// + +import SwiftUI +import TvManiac + +public class ObservableValue : ObservableObject { + @Published + var value: T + + private var cancellation: Cancellation? + + init(_ value: Value) { + self.value = value.value + self.cancellation = value.observe { [weak self] value in self?.value = value } + } + + deinit { + cancellation?.cancel() + } +} diff --git a/ios/ios/Ui/MockData/ShowMockData.swift b/ios/ios/Resources/MockData/ShowMockData.swift similarity index 89% rename from ios/ios/Ui/MockData/ShowMockData.swift rename to ios/ios/Resources/MockData/ShowMockData.swift index f3ed31077..ca9b273eb 100644 --- a/ios/ios/Ui/MockData/ShowMockData.swift +++ b/ios/ios/Resources/MockData/ShowMockData.swift @@ -54,13 +54,13 @@ var seasonList = [ Season(seasonId: 13, tvShowId: 13, name: "Season 2") ] -var detailState = ShowDetailsLoaded( +var detailState = ShowDetailsState( show: mockShow, isLoading: false, errorMessage: nil, - similarShowsContent: ShowDetailsLoaded.SimilarShowsContent.companion.EMPTY_SIMILAR_SHOWS, - seasonsContent: ShowDetailsLoaded.SeasonsContent.companion.EMPTY_SEASONS, - trailersContent: ShowDetailsLoaded.TrailersContent.companion.EMPTY_TRAILERS + similarShowsContent: ShowDetailsState.SimilarShowsContent.companion.EMPTY_SIMILAR_SHOWS, + seasonsContent: ShowDetailsState.SeasonsContent.companion.EMPTY_SEASONS, + trailersContent: ShowDetailsState.TrailersContent.companion.EMPTY_TRAILERS ) //Get rid of this class once we implement show detail stateMachine diff --git a/ios/ios/Root/RootView.swift b/ios/ios/Root/RootView.swift new file mode 100644 index 000000000..0802d8171 --- /dev/null +++ b/ios/ios/Root/RootView.swift @@ -0,0 +1,132 @@ +// +// RootView.swift +// tv-maniac +// +// Created by Thomas Kioko on 03.12.23. +// Copyright © 2023 orgName. All rights reserved. +// + +import SwiftUI +import TvManiac + +struct RootView: View { + + @StateValue + private var stack: ChildStack + + @StateValue + var uiState: ThemeState + let rootPresenter: RootNavigationPresenter + + init(rootPresenter: RootNavigationPresenter) { + self.rootPresenter = rootPresenter + _stack = StateValue(rootPresenter.screenStack) + _uiState = StateValue(rootPresenter.state) + } + + + + var body: some View { + ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)){ + let screen = stack.active.instance + + ChildView(screen: screen) + .frame(maxHeight: .infinity) + + //TODO:: Animate visibility based on the screen. Only show when it's a tab screen + BottomNavigation(screen, rootPresenter) + .background(.ultraThinMaterial) + + } + .preferredColorScheme(uiState.appTheme == AppTheme.lightTheme ? .light : uiState.appTheme == AppTheme.darkTheme ? .dark : nil) + } +} + +fileprivate func BottomNavigation(_ screen: Screen,_ rootPresenter: RootNavigationPresenter) -> some View { + return HStack(alignment: .bottom, spacing: 16) { + Spacer() + + BottomTabView( + title: "Discover", + systemImage: "film", + isActive: screen is ScreenDiscover, + action: { rootPresenter.bringToFront(config: RootNavigationPresenterConfigDiscover()) } + ) + + Spacer() + + BottomTabView( + title: "Search", + systemImage: "magnifyingglass", + isActive: screen is ScreenSearch, + action: { rootPresenter.bringToFront(config: RootNavigationPresenterConfigSearch()) } + ) + + Spacer() + BottomTabView( + title: "Library", + systemImage: "list.bullet.below.rectangle", + isActive: screen is ScreenLibrary, + action: { rootPresenter.bringToFront(config: RootNavigationPresenterConfigLibrary()) } + ) + Spacer() + + BottomTabView( + title: "Settings", + systemImage: "gearshape", + isActive: screen is ScreenSettings, + action: { rootPresenter.bringToFront(config: RootNavigationPresenterConfigSettings()) } + ) + Spacer() + + }.frame(width: UIScreen.main.bounds.width, height: 64) +} + +private struct ChildView: View { + let screen: Screen + + var body: some View { + switch screen { + case let screen as ScreenDiscover : DiscoverView(presenter: screen.presenter) + case let screen as ScreenSearch : SearchView(presenter: screen.presenter) + case let screen as ScreenLibrary : LibraryView(presenter: screen.presenter) + case let screen as ScreenSettings : SettingsView(presenter: screen.presenter) + case let screen as ScreenShowDetails: ShowDetailView(presenter: screen.presenter) + default: EmptyView() + } + } +} + + + + +private struct BottomTabView: View { + let title: String + let systemImage: String + let isActive: Bool + let action: () -> Void + + var body: some View { + + Button(action: action) { + VStack(alignment: .center) { + Image(systemName: systemImage) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(isActive ? .blue : .text_color_bg) + .font(Font.title.weight(.thin)) + .frame(width: 26, height: 26) + .animation(.default) + .opacity(isActive ? 1 : 0.5) + + Spacer().frame(height: 4) + + Text(title) + .foregroundColor(isActive ? .blue : .text_color_bg) + .bodyMediumFont(size: 14) + .fontWeight(.medium) + } + } + .buttonStyle(.plain) + } +} diff --git a/ios/ios/RootHolder.swift b/ios/ios/RootHolder.swift new file mode 100644 index 000000000..a31df9cf0 --- /dev/null +++ b/ios/ios/RootHolder.swift @@ -0,0 +1,24 @@ +// +// RootHolder.swift +// tv-maniac +// +// Created by Thomas Kioko on 03.12.23. +// Copyright © 2023 orgName. All rights reserved. +// + +import Foundation +import TvManiac + +class RootHolder : ObservableObject { + let lifecycle: LifecycleRegistry + + init() { + lifecycle = LifecycleRegistryKt.LifecycleRegistry() + LifecycleRegistryExtKt.create(lifecycle) + } + + deinit { + // Destroy the root component before it is deallocated + LifecycleRegistryExtKt.destroy(lifecycle) + } +} diff --git a/ios/ios/Search/SearchView.swift b/ios/ios/Search/SearchView.swift new file mode 100644 index 000000000..5f2085683 --- /dev/null +++ b/ios/ios/Search/SearchView.swift @@ -0,0 +1,40 @@ +// +// SearchView.swift +// tv-maniac +// +// Created by Thomas Kioko on 19.08.21. +// Copyright © 2021 orgName. All rights reserved. +// + +import SwiftUI +import TvManiac + +struct SearchView: View { + + private let presenter: SearchPresenter + + @StateValue + private var uiState: SearchState + @State private var query = String() + + init(presenter: SearchPresenter){ + self.presenter = presenter + _uiState = StateValue(presenter.state) + } + + var body: some View { + NavigationStack { + VStack { + + } + .background(Color.background) + .navigationTitle("Search") + .navigationBarTitleDisplayMode(.large) + .searchable(text: $query) + .task(id: query) { + if query.isEmpty { return } + if Task.isCancelled { return } + } + } + } +} diff --git a/ios/ios/Feature/Settings/SettingsUIView.swift b/ios/ios/Settings/SettingsView.swift similarity index 69% rename from ios/ios/Feature/Settings/SettingsUIView.swift rename to ios/ios/Settings/SettingsView.swift index 445fec3c0..55871360a 100644 --- a/ios/ios/Feature/Settings/SettingsUIView.swift +++ b/ios/ios/Settings/SettingsView.swift @@ -6,56 +6,62 @@ // Copyright © 2022 orgName. All rights reserved. // +import Foundation import SwiftUI import TvManiac -struct SettingsUIView: View { - - @StateObject var viewModel: SettingsViewModel = SettingsViewModel() - @ObservedObject private var model = TraktAuthViewModel() +struct SettingsView: View { + private let presenter: SettingsPresenter + @StateValue private var uiState: SettingsState @Environment(\.openURL) var openURL @Environment(\.presentationMode) var presentationMode - @SwiftUI.State private var showingAlert: Bool = false + @State private var theme: DeveiceAppTheme = DeveiceAppTheme.System + @State private var showingAlert: Bool = false + @ObservedObject private var model = TraktAuthViewModel() + + + init(presenter: SettingsPresenter){ + self.presenter = presenter + _uiState = StateValue(presenter.state) + theme = toAppTheme(theme: uiState.appTheme) + } var body: some View { - NavigationView { + NavigationStack { SettingsForm() - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button ( - action: { - self.presentationMode.wrappedValue.dismiss() - }, - label: { - LabelText(text: "Done") - } - ) - } + .navigationTitle("Settings") + .navigationBarTitleDisplayMode(.large) + .onAppear { + theme = toAppTheme(theme: uiState.appTheme) } } } - fileprivate func SettingsForm() -> Form, Section, some View)>> { + fileprivate func SettingsForm() -> Form, Section, some View + )>> { return Form { Section(header: Text("App Theme").bodyMediumFont(size: 16)) { Picker( - selection: $viewModel.appTheme, + selection: $theme, label: Text("Change Theme") .bodyMediumFont(size: 16), content: { - ForEach(AppTheme.allCases, id: \.self) { theme in + ForEach(DeveiceAppTheme.allCases, id: \.self) { theme in Text(theme.getName()) .tag(theme.rawValue) } }) - .onChange(of: viewModel.appTheme) { theme in - viewModel.dispatchAction(action: ThemeSelected(theme: theme.toTheme())) + .pickerStyle(.segmented) + .padding(.vertical, 6) + .onChange(of: theme) { theme in + presenter.dispatch(action: ThemeSelected(appTheme: toTheme(appTheme: theme))) } } @@ -66,13 +72,16 @@ struct SettingsUIView: View { title: "Connect to Trakt", description: "Trakt is a platform that does many things, but primarily keeps track of TV shows and movies you watch." ) { - showingAlert = true + showingAlert = !uiState.showTraktDialog } .alert(isPresented: $showingAlert) { Alert( title: Text("Trakt Coming Soon"), message: Text("Trakt is a platform that does many things, but primarily keeps track of TV shows and movies you watch."), - primaryButton: .default(Text("Login")) { model.initiateAuthorization() }, + primaryButton: .default(Text("Login")) { + model.initiateAuthorization() + presenter.dispatch(action: TraktLoginClicked_()) + }, secondaryButton: .destructive(Text("Cancel")) ) } @@ -89,10 +98,6 @@ struct SettingsUIView: View { } } .navigationBarTitle("Settings") - .background(Color.background) - .accentColor(Color.accent) - .onAppear { viewModel.startStateMachine() } - .onDisappear { viewModel.cancel() } } } @@ -124,7 +129,6 @@ struct SettingsItem: View { .padding(.top, 1.5) } } - .onTapGesture(perform: onClick) } } diff --git a/ios/ios/StackView.swift b/ios/ios/StackView.swift new file mode 100644 index 000000000..f317f85fb --- /dev/null +++ b/ios/ios/StackView.swift @@ -0,0 +1,39 @@ +// +// StackView.swift +// tv-maniac +// +// Created by Thomas Kioko on 04.12.23. +// Copyright © 2023 orgName. All rights reserved. +// + +import SwiftUI +import UIKit +import TvManiac + +// Source: https://github.com/arkivanov/Decompose/blob/master/sample/app-ios/app-ios/DecomposeHelpers/StackView.swift +struct StackView: View { + @ObservedObject + var stackValue: ObservableValue> + + var onBack: (_ newCount: Int32) -> Void + + @ViewBuilder + var childContent: (T) -> Content + + var stack: [Child] { stackValue.value.items } + + var body: some View { + NavigationStack( + path: Binding( + get: { stack.dropFirst() }, + set: { updatedPath in onBack(Int32(updatedPath.count)) } + ) + ) { + childContent(stack.first!.instance!) + .navigationDestination(for: Child.self) { + childContent($0.instance!) + } + } + } +} + diff --git a/ios/ios/StateValue.swift b/ios/ios/StateValue.swift new file mode 100644 index 000000000..756d35270 --- /dev/null +++ b/ios/ios/StateValue.swift @@ -0,0 +1,21 @@ +// +// StateValue.swift +// tv-maniac +// +// Created by Thomas Kioko on 03.12.23. +// Copyright © 2023 orgName. All rights reserved. +// + +import SwiftUI +import TvManiac + +@propertyWrapper struct StateValue: DynamicProperty { + @ObservedObject + private var obj: ObservableValue + + var wrappedValue: T { obj.value } + + init(_ value: Value) { + obj = ObservableValue(value) + } +} diff --git a/ios/ios/Feature/Settings/SettingsUtil.swift b/ios/ios/ThemeUtilities.swift similarity index 50% rename from ios/ios/Feature/Settings/SettingsUtil.swift rename to ios/ios/ThemeUtilities.swift index 9773c6bd5..325be97e4 100644 --- a/ios/ios/Feature/Settings/SettingsUtil.swift +++ b/ios/ios/ThemeUtilities.swift @@ -9,8 +9,9 @@ import Foundation import TvManiac import UIKit +import SwiftUI -enum AppTheme: Int, CaseIterable { +enum DeveiceAppTheme: Int, CaseIterable { case Light = 0 case Dark = 1 case System = 2 @@ -25,26 +26,27 @@ enum AppTheme: Int, CaseIterable { return "Dark Theme" } } - - func toTheme() -> Theme { - switch self { - case .System: - return Theme.system - case .Light: - return Theme.light - case .Dark: - return Theme.dark - } + +} + +func toTheme(appTheme: DeveiceAppTheme) -> AppTheme { + switch appTheme { + case .System: + return AppTheme.systemTheme + case .Light: + return AppTheme.lightTheme + case .Dark: + return AppTheme.darkTheme } } -func toAppTheme(theme: Theme) -> AppTheme { +func toAppTheme(theme: AppTheme) -> DeveiceAppTheme { switch theme { - case Theme.dark: - return AppTheme.Dark - case Theme.light: - return AppTheme.Light + case AppTheme.darkTheme: + return DeveiceAppTheme.Dark + case AppTheme.lightTheme: + return DeveiceAppTheme.Light default: - return AppTheme.System + return DeveiceAppTheme.System } } diff --git a/ios/ios/Ui/Components/SnapCarousel.swift b/ios/ios/Ui/Components/SnapCarousel.swift deleted file mode 100644 index 2f77a0bbb..000000000 --- a/ios/ios/Ui/Components/SnapCarousel.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// SnapCarousel.swift -// tv-maniac -// -// Created by Thomas Kioko on 16.01.22. -// Copyright © 2022 orgName. All rights reserved. -// - -import SwiftUI -import TvManiac - -// See my Custom Snap Carousel Video... -// Link in Description... - -struct SnapCarousel: View { - var content: (T) -> Content - var list: [T] - - // Properties... - var spacing: CGFloat - var trailingSpace: CGFloat - @Binding var index: Int - - init( - spacing: CGFloat = 15, - trailingSpace: CGFloat = 100, - index: Binding, - items: [T], - @ViewBuilder content: @escaping (T) -> Content - ) { - - self.list = items - self.spacing = spacing - self.trailingSpace = trailingSpace - self._index = index - self.content = content - } - - // Offset... - @GestureState var offset: CGFloat = 0 - @SwiftUI.State var currentIndex: Int = 2 - - var body: some View { - - GeometryReader { proxy in - - let width = proxy.size.width - (trailingSpace - spacing) - let adjustmentWidth = (trailingSpace / 2) - spacing - - HStack(spacing: spacing) { - - ForEach(list, id: \.self) { item in - content(item) - .frame(width: proxy.size.width - trailingSpace) - .offset(y: getOffset(item: item, width: width)) - } - } - // Spacing will be horizontal padding... - .padding(.horizontal, spacing) - // setting only after 0th index.. - .offset(x: (CGFloat(currentIndex) * -width) + (currentIndex != 0 ? adjustmentWidth : 0) + offset) - .gesture( - - DragGesture() - .updating($offset, body: { value, out, _ in - - out = value.translation.width - }) - .onEnded({ value in - - // Updating Current Index.... - let offsetX = value.translation.width - - // were going to convert the tranlsation into progress (0 - 1) - // and round the value... - // based on the progress increasing or decreasing the currentIndex... - - let progress = -offsetX / width - - let roundIndex = progress.rounded() - - // setting min... - currentIndex = max(min(currentIndex + Int(roundIndex), list.count - 1), 0) - - // updating index.... - currentIndex = index - }) - .onChanged({ value in - // updating only index.... - - // Updating Current Index.... - let offsetX = value.translation.width - - // were going to convert the tranlsation into progress (0 - 1) - // and round the value... - // based on the progress increasing or decreasing the currentIndex... - - let progress = -offsetX / width - - let roundIndex = progress.rounded() - - // setting min... - index = max(min(currentIndex + Int(roundIndex), list.count - 1), 0) - }) - ) - } - // Animating when offset = 0 - .animation(.easeInOut, value: offset == 0) - } - - // Moving View based on scroll Offset... - func getOffset(item: T, width: CGFloat) -> CGFloat { - - // Progress... - // Shifting Current Item to Top.... - let progress = ((offset < 0 ? offset : -offset) / width) * 60 - - // max 60... - // then again minus from 60.... - let topOffset = -progress < 60 ? progress : -(progress + 120) - - let previous = getIndex(item: item) - 1 == currentIndex ? (offset < 0 ? topOffset : -topOffset) : 0 - - let next = getIndex(item: item) + 1 == currentIndex ? (offset < 0 ? -topOffset : topOffset) : 0 - - // Saftey check between 0 to max list size... - let checkBetween = currentIndex >= 0 && currentIndex < list.count ? (getIndex(item: item) - 1 == currentIndex ? previous : next) : 0 - - // checking current.... - // if so shifting view to top... - return getIndex(item: item) == currentIndex ? -60 - topOffset : checkBetween - } - - // Fetching Index... - func getIndex(item: T) -> Int { - let index = list.firstIndex { currentItem in - return currentItem.traktId == item.traktId - } ?? 0 - - return index - } -} diff --git a/ios/ios/Ui/Extensions/ContentHostingController.swift b/ios/ios/Ui/Extensions/ContentHostingController.swift deleted file mode 100644 index b3937e871..000000000 --- a/ios/ios/Ui/Extensions/ContentHostingController.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// ContentHostingController.swift -// tv-maniac -// -// Created by Thomas Kioko on 15.01.22. -// Copyright © 2022 orgName. All rights reserved. -// - -import UIKit -import SwiftUI - -extension View { - /// A view modifier to set the color of the iOS Status Bar - func statusBarStyle(_ style: UIStatusBarStyle, ignoreDarkMode: Bool = false) -> some View { - background(HostingWindowFinder(callback: { window in - guard let rootViewController = window?.rootViewController else { return } - let hostingController = HostingViewController(rootViewController: rootViewController, style: style, ignoreDarkMode: ignoreDarkMode) - window?.rootViewController = hostingController - })) - } -} - -fileprivate class HostingViewController: UIViewController { - private var rootViewController: UIViewController? - private var style: UIStatusBarStyle = .lightContent - private var ignoreDarkMode: Bool = false - - init(rootViewController: UIViewController, style: UIStatusBarStyle, ignoreDarkMode: Bool) { - self.rootViewController = rootViewController - self.style = style - self.ignoreDarkMode = ignoreDarkMode - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - } - - override func viewDidLoad() { - super.viewDidLoad() - guard let child = rootViewController else { return } - addChild(child) - view.addSubview(child.view) - child.didMove(toParent: self) - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - if ignoreDarkMode || traitCollection.userInterfaceStyle == .light { - return style - } else { - if style == .darkContent { - return .lightContent - } else { - return .darkContent - } - } - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - setNeedsStatusBarAppearanceUpdate() - } -} - -fileprivate struct HostingWindowFinder: UIViewRepresentable { - var callback: (UIWindow?) -> () - - func makeUIView(context: Context) -> UIView { - let view = UIView() - DispatchQueue.main.async { [weak view] in - self.callback(view?.window) - } - return view - } - - func updateUIView(_ uiView: UIView, context: Context) { - // NO-OP - } -} diff --git a/ios/ios/Ui/Extensions/UINavigationController.swift b/ios/ios/Ui/Extensions/UINavigationController.swift deleted file mode 100644 index a2d2b5c6e..000000000 --- a/ios/ios/Ui/Extensions/UINavigationController.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ColorExtension.swift -// tv-maniac -// -// Created by Thomas Kioko on 15.01.22. -// Copyright © 2022 orgName. All rights reserved. -// - -import SwiftUI - -extension UINavigationController { - // Remove back button text - open override func viewWillLayoutSubviews() { - navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) - } -} diff --git a/ios/ios/iOSApp.swift b/ios/ios/iOSApp.swift index 6eaf549b4..bc3ac1424 100644 --- a/ios/ios/iOSApp.swift +++ b/ios/ios/iOSApp.swift @@ -3,29 +3,27 @@ import TvManiac @main struct iOSApp: App { + + @UIApplicationDelegateAdaptor(AppDelegate.self) + var appDelegate: AppDelegate + + @Environment(\.scenePhase) var scenePhase: ScenePhase + + var rootHolder: RootHolder { appDelegate.rootHolder } + + var body: some Scene { + + WindowGroup { + RootView(rootPresenter: appDelegate.iosViewPresenter.presenter) + .onChange(of: scenePhase) { newPhase in + switch newPhase { + case .background: LifecycleRegistryExtKt.stop(rootHolder.lifecycle) + case .inactive: LifecycleRegistryExtKt.pause(rootHolder.lifecycle) + case .active: LifecycleRegistryExtKt.resume(rootHolder.lifecycle) + @unknown default: break + } + } + } + } - @StateObject var viewModel: SettingsViewModel = SettingsViewModel() - @Environment(\.colorScheme) var systemColorScheme: ColorScheme - - var body: some Scene { - WindowGroup { - HomeUIView() - .preferredColorScheme(colorScheme) - .onAppear { viewModel.startStateMachine() } - .environmentObject(viewModel) - } - } - - var colorScheme: ColorScheme? { - withAnimation { - switch viewModel.appTheme { - case .Dark: - return .dark - case .Light: - return .light - default: - return .light - } - } - } } diff --git a/ios/tv-maniac.xcodeproj/project.pbxproj b/ios/tv-maniac.xcodeproj/project.pbxproj index 4b7f8b23b..ab4199a74 100644 --- a/ios/tv-maniac.xcodeproj/project.pbxproj +++ b/ios/tv-maniac.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -11,7 +11,7 @@ CBDFC5CAEA5938940871491E /* OffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDFC8F86D71D33205E42D7F /* OffsetModifier.swift */; }; CBDFC60AF859A44365A90BA5 /* TopNavBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDFC4463ABF8E9F9AEB156C /* TopNavBar.swift */; }; CBDFCA9D24126097D232D988 /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDFC78A5C2441FF7F2BB401 /* BlurView.swift */; }; - CBDFCFDED77DDE3DC67E3C28 /* ShowDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDFCB6603736674C1A12CEE /* ShowDetailsViewModel.swift */; }; + D60C64422B249B33006B401C /* TvManiac in Frameworks */ = {isa = PBXBuildFile; productRef = D60C64412B249B33006B401C /* TvManiac */; }; D628B5C02A0C167E00015E45 /* config.yaml in Resources */ = {isa = PBXBuildFile; fileRef = D628B5BF2A0C167E00015E45 /* config.yaml */; }; D628B5C42A0C171300015E45 /* dev.yaml in Resources */ = {isa = PBXBuildFile; fileRef = D628B5C32A0C171300015E45 /* dev.yaml */; }; D64279562A6080A900E65755 /* ToastStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64279552A6080A900E65755 /* ToastStyle.swift */; }; @@ -27,17 +27,18 @@ D653925B29DAC6A5000EE673 /* UnauthenticatedProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653925A29DAC6A5000EE673 /* UnauthenticatedProfileView.swift */; }; D653925D29DAC89D000EE673 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653925C29DAC89D000EE673 /* ProfileView.swift */; }; D653926029DAC949000EE673 /* AuthenticatedProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653925F29DAC949000EE673 /* AuthenticatedProfileView.swift */; }; - D653926329DACC39000EE673 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653926229DACC39000EE673 /* ProfileViewModel.swift */; }; D653926B29DAD8FA000EE673 /* config.json in Resources */ = {isa = PBXBuildFile; fileRef = D653926A29DAD8FA000EE673 /* config.json */; }; + D667A42D2B23D897009C951E /* FeaturedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667A42C2B23D897009C951E /* FeaturedView.swift */; }; + D67761852B1CE6AE00537DD5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67761842B1CE6AE00537DD5 /* AppDelegate.swift */; }; + D67761872B1CE6F600537DD5 /* RootHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67761862B1CE6F600537DD5 /* RootHolder.swift */; }; + D67761892B1CED9E00537DD5 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67761882B1CED9E00537DD5 /* RootView.swift */; }; + D677618E2B1CF25A00537DD5 /* StateValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677618D2B1CF25A00537DD5 /* StateValue.swift */; }; + D67761902B1CF28A00537DD5 /* ObservableValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677618F2B1CF28A00537DD5 /* ObservableValue.swift */; }; + D67761982B1D458B00537DD5 /* StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67761972B1D458B00537DD5 /* StackView.swift */; }; + D677619A2B1D4BA200537DD5 /* EmptyUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67761992B1D4BA200537DD5 /* EmptyUIView.swift */; }; D6F736AA2A61B830007EE1FB /* DetailScreenHelperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F736A92A61B830007EE1FB /* DetailScreenHelperView.swift */; }; - D6F736AF2A67E12E007EE1FB /* TvManiac in Frameworks */ = {isa = PBXBuildFile; productRef = D6F736AE2A67E12E007EE1FB /* TvManiac */; }; - E90392B02918616400B9CAF0 /* DiscoverShowsViewmodel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90392AF2918616400B9CAF0 /* DiscoverShowsViewmodel.swift */; }; E90392C129197BDF00B9CAF0 /* FullScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90392C029197BDF00B9CAF0 /* FullScreenView.swift */; }; - E924E11D272F2BA000C4435F /* HomeUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E924E11C272F2BA000C4435F /* HomeUIView.swift */; }; E983816D2793697B0039CB08 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98381692793697B0039CB08 /* ColorExtension.swift */; }; - E983816E2793697B0039CB08 /* ContentHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983816A2793697B0039CB08 /* ContentHostingController.swift */; }; - E983816F2793697B0039CB08 /* OverlayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983816B2793697B0039CB08 /* OverlayExtension.swift */; }; - E98381702793697B0039CB08 /* UINavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983816C2793697B0039CB08 /* UINavigationController.swift */; }; E983818E27936BB10039CB08 /* LoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983818327936BAF0039CB08 /* LoadingIndicatorView.swift */; }; E983819127936BB10039CB08 /* ShowPosterImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983818627936BB00039CB08 /* ShowPosterImage.swift */; }; E983819227936BB10039CB08 /* BorderedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983818727936BB00039CB08 /* BorderedButton.swift */; }; @@ -62,13 +63,12 @@ E98381D02793A0A50039CB08 /* WorkSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E98381C72793A0A50039CB08 /* WorkSans-Medium.ttf */; }; E98381D227942D720039CB08 /* ShowBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98381D127942D720039CB08 /* ShowBodyView.swift */; }; E98381E127943F940039CB08 /* SnapCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98381E027943F940039CB08 /* SnapCarousel.swift */; }; - E989E1162940E78E00C01A39 /* SettingsUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E989E1152940E78E00C01A39 /* SettingsUIView.swift */; }; - E989E1182940EAB300C01A39 /* SettingsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = E989E1172940EAB300C01A39 /* SettingsUtil.swift */; }; - E989E11A2940EAFA00C01A39 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E989E1192940EAFA00C01A39 /* SettingsViewModel.swift */; }; + E989E1162940E78E00C01A39 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E989E1152940E78E00C01A39 /* SettingsView.swift */; }; + E989E1182940EAB300C01A39 /* ThemeUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E989E1172940EAB300C01A39 /* ThemeUtilities.swift */; }; E9AC834C26CEE00D00829A0D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E9AC834B26CEE00D00829A0D /* Assets.xcassets */; }; E9AC835126CEEA1500829A0D /* DiscoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AC835026CEEA1500829A0D /* DiscoverView.swift */; }; E9AC835326CEEA9800829A0D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AC835226CEEA9800829A0D /* SearchView.swift */; }; - E9AC835526CEEAB800829A0D /* WatchlistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AC835426CEEAB800829A0D /* WatchlistView.swift */; }; + E9AC835526CEEAB800829A0D /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AC835426CEEAB800829A0D /* LibraryView.swift */; }; E9C6114E27FF863600F8A23F /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = E9C6114D27FF863600F8A23F /* Kingfisher */; }; E9CD0B2527AC7D140021516B /* PrintExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9CD0B2427AC7D140021516B /* PrintExtension.swift */; }; /* End PBXBuildFile section */ @@ -80,7 +80,6 @@ CBDFC4463ABF8E9F9AEB156C /* TopNavBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopNavBar.swift; sourceTree = ""; }; CBDFC78A5C2441FF7F2BB401 /* BlurView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurView.swift; sourceTree = ""; }; CBDFC8F86D71D33205E42D7F /* OffsetModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OffsetModifier.swift; sourceTree = ""; }; - CBDFCB6603736674C1A12CEE /* ShowDetailsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShowDetailsViewModel.swift; sourceTree = ""; }; D628B5BF2A0C167E00015E45 /* config.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; name = config.yaml; path = ../../../core/util/src/commonMain/resources/config.yaml; sourceTree = ""; }; D628B5C32A0C171300015E45 /* dev.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = dev.yaml; sourceTree = ""; }; D64279552A6080A900E65755 /* ToastStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastStyle.swift; sourceTree = ""; }; @@ -95,17 +94,19 @@ D653925A29DAC6A5000EE673 /* UnauthenticatedProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnauthenticatedProfileView.swift; sourceTree = ""; }; D653925C29DAC89D000EE673 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; D653925F29DAC949000EE673 /* AuthenticatedProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedProfileView.swift; sourceTree = ""; }; - D653926229DACC39000EE673 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; D653926A29DAD8FA000EE673 /* config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = config.json; sourceTree = ""; }; + D667A42C2B23D897009C951E /* FeaturedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedView.swift; sourceTree = ""; }; + D67761842B1CE6AE00537DD5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D67761862B1CE6F600537DD5 /* RootHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootHolder.swift; sourceTree = ""; }; + D67761882B1CED9E00537DD5 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; + D677618D2B1CF25A00537DD5 /* StateValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateValue.swift; sourceTree = ""; }; + D677618F2B1CF28A00537DD5 /* ObservableValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableValue.swift; sourceTree = ""; }; + D67761972B1D458B00537DD5 /* StackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackView.swift; sourceTree = ""; }; + D67761992B1D4BA200537DD5 /* EmptyUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUIView.swift; sourceTree = ""; }; + D67761A12B1E59B600537DD5 /* TvManiac.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = TvManiac.xcframework; path = "../../tvmaniac-swift-packages/TvManiac.xcframework"; sourceTree = ""; }; D6F736A92A61B830007EE1FB /* DetailScreenHelperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailScreenHelperView.swift; sourceTree = ""; }; - D6F736AB2A61C399007EE1FB /* TvManiac.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = TvManiac.xcframework; path = "../../tvmaniac-swift-packages/TvManiac.xcframework"; sourceTree = ""; }; - E90392AF2918616400B9CAF0 /* DiscoverShowsViewmodel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoverShowsViewmodel.swift; sourceTree = ""; }; E90392C029197BDF00B9CAF0 /* FullScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenView.swift; sourceTree = ""; }; - E924E11C272F2BA000C4435F /* HomeUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeUIView.swift; sourceTree = ""; }; E98381692793697B0039CB08 /* ColorExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; - E983816A2793697B0039CB08 /* ContentHostingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentHostingController.swift; sourceTree = ""; }; - E983816B2793697B0039CB08 /* OverlayExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayExtension.swift; sourceTree = ""; }; - E983816C2793697B0039CB08 /* UINavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UINavigationController.swift; sourceTree = ""; }; E983818327936BAF0039CB08 /* LoadingIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorView.swift; sourceTree = ""; }; E983818627936BB00039CB08 /* ShowPosterImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShowPosterImage.swift; sourceTree = ""; }; E983818727936BB00039CB08 /* BorderedButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BorderedButton.swift; sourceTree = ""; }; @@ -129,14 +130,13 @@ E98381C62793A0A50039CB08 /* WorkSans-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "WorkSans-SemiBold.ttf"; sourceTree = ""; }; E98381C72793A0A50039CB08 /* WorkSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "WorkSans-Medium.ttf"; sourceTree = ""; }; E98381D127942D720039CB08 /* ShowBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowBodyView.swift; sourceTree = ""; }; - E98381E027943F940039CB08 /* SnapCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapCarousel.swift; sourceTree = ""; }; - E989E1152940E78E00C01A39 /* SettingsUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUIView.swift; sourceTree = ""; }; - E989E1172940EAB300C01A39 /* SettingsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUtil.swift; sourceTree = ""; }; - E989E1192940EAFA00C01A39 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; + E98381E027943F940039CB08 /* SnapCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SnapCarousel.swift; path = ../Components/SnapCarousel.swift; sourceTree = ""; }; + E989E1152940E78E00C01A39 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + E989E1172940EAB300C01A39 /* ThemeUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeUtilities.swift; sourceTree = ""; }; E9AC834B26CEE00D00829A0D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; E9AC835026CEEA1500829A0D /* DiscoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoverView.swift; sourceTree = ""; }; E9AC835226CEEA9800829A0D /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; - E9AC835426CEEAB800829A0D /* WatchlistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistView.swift; sourceTree = ""; }; + E9AC835426CEEAB800829A0D /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; E9CD0B2427AC7D140021516B /* PrintExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintExtension.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -147,7 +147,7 @@ files = ( E9C6114E27FF863600F8A23F /* Kingfisher in Frameworks */, D653924829D8B6A6000EE673 /* OAuthSwift in Frameworks */, - D6F736AF2A67E12E007EE1FB /* TvManiac in Frameworks */, + D60C64422B249B33006B401C /* TvManiac in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -174,14 +174,16 @@ 7555FF7D242A565900829871 /* ios */ = { isa = PBXGroup; children = ( - D628B5BE2A0C165400015E45 /* Resources */, - D653925129DABCBA000EE673 /* Configuration */, - E9B6868F278736FA001698B7 /* Feature */, - E9B6869327873774001698B7 /* Ui */, - E9AC834B26CEE00D00829A0D /* Assets.xcassets */, - 7555FF8C242A565B00829871 /* Info.plist */, D653926A29DAD8FA000EE673 /* config.json */, + 7555FF8C242A565B00829871 /* Info.plist */, 2152FB032600AC8F00CF470E /* iOSApp.swift */, + E9AC834B26CEE00D00829A0D /* Assets.xcassets */, + D653925129DABCBA000EE673 /* Configuration */, + E9838168279369680039CB08 /* Extensions */, + E9B6868F278736FA001698B7 /* Feature */, + D628B5BE2A0C165400015E45 /* Resources */, + D68FFDD92B1FC89C001EB447 /* Utils */, + D68FFDDE2B21162A001EB447 /* View */, ); path = ios; sourceTree = ""; @@ -189,6 +191,7 @@ D628B5BE2A0C165400015E45 /* Resources */ = { isa = PBXGroup; children = ( + E98381A327936DFE0039CB08 /* MockData */, D628B5C32A0C171300015E45 /* dev.yaml */, D628B5BF2A0C167E00015E45 /* config.yaml */, ); @@ -213,7 +216,6 @@ D653925E29DAC911000EE673 /* Authenticated */, D653925929DAC627000EE673 /* Unauthenticated */, D653925C29DAC89D000EE673 /* ProfileView.swift */, - D653926229DACC39000EE673 /* ProfileViewModel.swift */, ); path = Profile; sourceTree = ""; @@ -253,14 +255,79 @@ path = TraktAuth; sourceTree = ""; }; + D667A42B2B23D86E009C951E /* Featured */ = { + isa = PBXGroup; + children = ( + D667A42C2B23D897009C951E /* FeaturedView.swift */, + ); + path = Featured; + sourceTree = ""; + }; + D667A42E2B23DC0E009C951E /* Root */ = { + isa = PBXGroup; + children = ( + D67761882B1CED9E00537DD5 /* RootView.swift */, + ); + path = Root; + sourceTree = ""; + }; + D677618C2B1CF22500537DD5 /* DecomposeHelpers */ = { + isa = PBXGroup; + children = ( + D67761862B1CE6F600537DD5 /* RootHolder.swift */, + D67761842B1CE6AE00537DD5 /* AppDelegate.swift */, + D67761972B1D458B00537DD5 /* StackView.swift */, + D677618D2B1CF25A00537DD5 /* StateValue.swift */, + D677618F2B1CF28A00537DD5 /* ObservableValue.swift */, + ); + name = DecomposeHelpers; + sourceTree = ""; + }; + D68FFDD92B1FC89C001EB447 /* Utils */ = { + isa = PBXGroup; + children = ( + D677618C2B1CF22500537DD5 /* DecomposeHelpers */, + E989E1172940EAB300C01A39 /* ThemeUtilities.swift */, + ); + name = Utils; + sourceTree = ""; + }; + D68FFDDE2B21162A001EB447 /* View */ = { + isa = PBXGroup; + children = ( + E9B686942787377A001698B7 /* Components */, + E9B6869027873707001698B7 /* Discover */, + D667A42B2B23D86E009C951E /* Featured */, + E9B6869227873747001698B7 /* Library */, + D667A42E2B23DC0E009C951E /* Root */, + E9B6869127873738001698B7 /* Search */, + D68FFDDF2B211690001EB447 /* Settings */, + ); + name = View; + sourceTree = ""; + }; + D68FFDDF2B211690001EB447 /* Settings */ = { + isa = PBXGroup; + children = ( + E989E1152940E78E00C01A39 /* SettingsView.swift */, + ); + path = Settings; + sourceTree = ""; + }; + D6BB5CF22B23784300D542D4 /* Components */ = { + isa = PBXGroup; + children = ( + E98381E027943F940039CB08 /* SnapCarousel.swift */, + ); + name = Components; + sourceTree = ""; + }; E9838168279369680039CB08 /* Extensions */ = { isa = PBXGroup; children = ( + E983817127936B420039CB08 /* Font */, E983818827936BB00039CB08 /* Font.swift */, E98381692793697B0039CB08 /* ColorExtension.swift */, - E983816A2793697B0039CB08 /* ContentHostingController.swift */, - E983816B2793697B0039CB08 /* OverlayExtension.swift */, - E983816C2793697B0039CB08 /* UINavigationController.swift */, E9CD0B2427AC7D140021516B /* PrintExtension.swift */, ); path = Extensions; @@ -291,7 +358,6 @@ E98381AA2793720C0039CB08 /* ShowInfoRow.swift */, E98381D127942D720039CB08 /* ShowBodyView.swift */, CBDFC4463ABF8E9F9AEB156C /* TopNavBar.swift */, - CBDFCB6603736674C1A12CEE /* ShowDetailsViewModel.swift */, ); path = Detail; sourceTree = ""; @@ -312,20 +378,10 @@ path = Grid; sourceTree = ""; }; - E989E1142940E76000C01A39 /* Settings */ = { - isa = PBXGroup; - children = ( - E989E1152940E78E00C01A39 /* SettingsUIView.swift */, - E989E1172940EAB300C01A39 /* SettingsUtil.swift */, - E989E1192940EAFA00C01A39 /* SettingsViewModel.swift */, - ); - path = Settings; - sourceTree = ""; - }; E9B6866E27871C99001698B7 /* Frameworks */ = { isa = PBXGroup; children = ( - D6F736AB2A61C399007EE1FB /* TvManiac.xcframework */, + D67761A12B1E59B600537DD5 /* TvManiac.xcframework */, ); name = Frameworks; sourceTree = ""; @@ -335,12 +391,7 @@ children = ( E98381AE27937B860039CB08 /* Grid */, E983819927936D8E0039CB08 /* Detail */, - E9B6869D27873CF2001698B7 /* Home */, - E9B6869227873747001698B7 /* Watchlist */, - E9B6869127873738001698B7 /* Search */, - E9B6869027873707001698B7 /* Discover */, D653924929D8B6B8000EE673 /* Profile */, - E989E1142940E76000C01A39 /* Settings */, ); path = Feature; sourceTree = ""; @@ -348,8 +399,8 @@ E9B6869027873707001698B7 /* Discover */ = { isa = PBXGroup; children = ( + D6BB5CF22B23784300D542D4 /* Components */, E9AC835026CEEA1500829A0D /* DiscoverView.swift */, - E90392AF2918616400B9CAF0 /* DiscoverShowsViewmodel.swift */, ); path = Discover; sourceTree = ""; @@ -362,28 +413,19 @@ path = Search; sourceTree = ""; }; - E9B6869227873747001698B7 /* Watchlist */ = { + E9B6869227873747001698B7 /* Library */ = { isa = PBXGroup; children = ( - E9AC835426CEEAB800829A0D /* WatchlistView.swift */, + E9AC835426CEEAB800829A0D /* LibraryView.swift */, ); - name = Watchlist; - sourceTree = ""; - }; - E9B6869327873774001698B7 /* Ui */ = { - isa = PBXGroup; - children = ( - E98381A327936DFE0039CB08 /* MockData */, - E983817127936B420039CB08 /* Font */, - E9838168279369680039CB08 /* Extensions */, - E9B686942787377A001698B7 /* Components */, - ); - path = Ui; + name = Library; + path = Feature; sourceTree = ""; }; E9B686942787377A001698B7 /* Components */ = { isa = PBXGroup; children = ( + D67761992B1D4BA200537DD5 /* EmptyUIView.swift */, D6F736A92A61B830007EE1FB /* DetailScreenHelperView.swift */, E983818727936BB00039CB08 /* BorderedButton.swift */, E983818327936BAF0039CB08 /* LoadingIndicatorView.swift */, @@ -391,7 +433,6 @@ E983818627936BB00039CB08 /* ShowPosterImage.swift */, E983818A27936BB00039CB08 /* ShowRow.swift */, E983818C27936BB10039CB08 /* TextViews.swift */, - E98381E027943F940039CB08 /* SnapCarousel.swift */, CBDFC78A5C2441FF7F2BB401 /* BlurView.swift */, CBDFC8F86D71D33205E42D7F /* OffsetModifier.swift */, E90392C029197BDF00B9CAF0 /* FullScreenView.swift */, @@ -400,14 +441,6 @@ path = Components; sourceTree = ""; }; - E9B6869D27873CF2001698B7 /* Home */ = { - isa = PBXGroup; - children = ( - E924E11C272F2BA000C4435F /* HomeUIView.swift */, - ); - path = Home; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -427,7 +460,7 @@ packageProductDependencies = ( E9C6114D27FF863600F8A23F /* Kingfisher */, D653924729D8B6A6000EE673 /* OAuthSwift */, - D6F736AE2A67E12E007EE1FB /* TvManiac */, + D60C64412B249B33006B401C /* TvManiac */, ); productName = ios; productReference = 7555FF7B242A565900829871 /* tv-maniac.app */; @@ -460,7 +493,8 @@ packageReferences = ( E9C6114C27FF863600F8A23F /* XCRemoteSwiftPackageReference "Kingfisher" */, D653924629D8B6A6000EE673 /* XCRemoteSwiftPackageReference "OAuthSwift" */, - D6F736AD2A67E12E007EE1FB /* XCRemoteSwiftPackageReference "tvmaniac-swift-packages" */, + D68666B12B1E7F2E00D6D334 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, + D60C64402B249B33006B401C /* XCRemoteSwiftPackageReference "tvmaniac-swift-packages" */, ); productRefGroup = 7555FF7C242A565900829871 /* Products */; projectDirPath = ""; @@ -500,16 +534,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E983816F2793697B0039CB08 /* OverlayExtension.swift in Sources */, E98381AB2793720C0039CB08 /* ShowInfoRow.swift in Sources */, - E924E11D272F2BA000C4435F /* HomeUIView.swift in Sources */, - E9AC835526CEEAB800829A0D /* WatchlistView.swift in Sources */, + E9AC835526CEEAB800829A0D /* LibraryView.swift in Sources */, D642795A2A6080F400E65755 /* ToastView.swift in Sources */, + D667A42D2B23D897009C951E /* FeaturedView.swift in Sources */, D653925329DABCEA000EE673 /* ConfigLoader.swift in Sources */, E983819727936BB10039CB08 /* TextViews.swift in Sources */, - E989E1162940E78E00C01A39 /* SettingsUIView.swift in Sources */, + E989E1162940E78E00C01A39 /* SettingsView.swift in Sources */, E98381A127936DC90039CB08 /* ShowDetailView.swift in Sources */, E98381B027937B970039CB08 /* ShowGridView.swift in Sources */, + D677619A2B1D4BA200537DD5 /* EmptyUIView.swift in Sources */, D653925B29DAC6A5000EE673 /* UnauthenticatedProfileView.swift in Sources */, E98381D227942D720039CB08 /* ShowBodyView.swift in Sources */, D6F736AA2A61B830007EE1FB /* DetailScreenHelperView.swift in Sources */, @@ -517,13 +551,16 @@ E983818E27936BB10039CB08 /* LoadingIndicatorView.swift in Sources */, D653924D29D8B70E000EE673 /* TraktManagerError.swift in Sources */, E98381A227936DC90039CB08 /* GenresRowView.swift in Sources */, + D67761982B1D458B00537DD5 /* StackView.swift in Sources */, E983819227936BB10039CB08 /* BorderedButton.swift in Sources */, E90392C129197BDF00B9CAF0 /* FullScreenView.swift in Sources */, - E989E1182940EAB300C01A39 /* SettingsUtil.swift in Sources */, - D653926329DACC39000EE673 /* ProfileViewModel.swift in Sources */, + E989E1182940EAB300C01A39 /* ThemeUtilities.swift in Sources */, D64279582A6080D400E65755 /* Toast.swift in Sources */, - E98381702793697B0039CB08 /* UINavigationController.swift in Sources */, + D67761902B1CF28A00537DD5 /* ObservableValue.swift in Sources */, + D67761872B1CE6F600537DD5 /* RootHolder.swift in Sources */, + D67761892B1CED9E00537DD5 /* RootView.swift in Sources */, 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, + D67761852B1CE6AE00537DD5 /* AppDelegate.swift in Sources */, D64279562A6080A900E65755 /* ToastStyle.swift in Sources */, E9CD0B2527AC7D140021516B /* PrintExtension.swift in Sources */, E98381A527936E150039CB08 /* ShowMockData.swift in Sources */, @@ -532,19 +569,16 @@ E983819527936BB10039CB08 /* ShowRow.swift in Sources */, E983819127936BB10039CB08 /* ShowPosterImage.swift in Sources */, E98381E127943F940039CB08 /* SnapCarousel.swift in Sources */, - E90392B02918616400B9CAF0 /* DiscoverShowsViewmodel.swift in Sources */, - E983816E2793697B0039CB08 /* ContentHostingController.swift in Sources */, D653924F29D8BC7E000EE673 /* TraktAuthViewModel.swift in Sources */, E983819427936BB10039CB08 /* PosterStyle.swift in Sources */, D642795C2A60814000E65755 /* ToastModifier.swift in Sources */, + D677618E2B1CF25A00537DD5 /* StateValue.swift in Sources */, E983816D2793697B0039CB08 /* ColorExtension.swift in Sources */, D653925529DABD04000EE673 /* Config.swift in Sources */, CBDFCA9D24126097D232D988 /* BlurView.swift in Sources */, CBDFC5CAEA5938940871491E /* OffsetModifier.swift in Sources */, D653926029DAC949000EE673 /* AuthenticatedProfileView.swift in Sources */, CBDFC60AF859A44365A90BA5 /* TopNavBar.swift in Sources */, - CBDFCFDED77DDE3DC67E3C28 /* ShowDetailsViewModel.swift in Sources */, - E989E11A2940EAFA00C01A39 /* SettingsViewModel.swift in Sources */, D653925D29DAC89D000EE673 /* ProfileView.swift in Sources */, D653925729DABD8F000EE673 /* ApplicationError.swift in Sources */, ); @@ -605,7 +639,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -661,7 +695,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -691,6 +725,7 @@ "$(inherited)", "-ObjC", "-l\"c++\"", + "-lsqlite3", ); PRODUCT_BUNDLE_IDENTIFIER = com.thomaskioko.tvmaniac; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -723,6 +758,7 @@ "$(inherited)", "-ObjC", "-l\"c++\"", + "-lsqlite3", ); PRODUCT_BUNDLE_IDENTIFIER = com.thomaskioko.tvmaniac; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -759,6 +795,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + D60C64402B249B33006B401C /* XCRemoteSwiftPackageReference "tvmaniac-swift-packages" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/thomaskioko/tvmaniac-swift-packages"; + requirement = { + branch = main; + kind = branch; + }; + }; D653924629D8B6A6000EE673 /* XCRemoteSwiftPackageReference "OAuthSwift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/OAuthSwift/OAuthSwift"; @@ -767,12 +811,12 @@ minimumVersion = 2.2.0; }; }; - D6F736AD2A67E12E007EE1FB /* XCRemoteSwiftPackageReference "tvmaniac-swift-packages" */ = { + D68666B12B1E7F2E00D6D334 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/c0de-wizard/tvmaniac-swift-packages"; + repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.8.0; + minimumVersion = 2.2.5; }; }; E9C6114C27FF863600F8A23F /* XCRemoteSwiftPackageReference "Kingfisher" */ = { @@ -786,16 +830,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + D60C64412B249B33006B401C /* TvManiac */ = { + isa = XCSwiftPackageProductDependency; + package = D60C64402B249B33006B401C /* XCRemoteSwiftPackageReference "tvmaniac-swift-packages" */; + productName = TvManiac; + }; D653924729D8B6A6000EE673 /* OAuthSwift */ = { isa = XCSwiftPackageProductDependency; package = D653924629D8B6A6000EE673 /* XCRemoteSwiftPackageReference "OAuthSwift" */; productName = OAuthSwift; }; - D6F736AE2A67E12E007EE1FB /* TvManiac */ = { - isa = XCSwiftPackageProductDependency; - package = D6F736AD2A67E12E007EE1FB /* XCRemoteSwiftPackageReference "tvmaniac-swift-packages" */; - productName = TvManiac; - }; E9C6114D27FF863600F8A23F /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; package = E9C6114C27FF863600F8A23F /* XCRemoteSwiftPackageReference "Kingfisher" */; diff --git a/ios/tv-maniac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/tv-maniac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a5eca5356..1c910a9b5 100644 --- a/ios/tv-maniac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/tv-maniac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -66,10 +66,10 @@ { "identity" : "tvmaniac-swift-packages", "kind" : "remoteSourceControl", - "location" : "https://github.com/c0de-wizard/tvmaniac-swift-packages", + "location" : "https://github.com/thomaskioko/tvmaniac-swift-packages", "state" : { - "revision" : "f2ef90ff48c42377e40f2a6b0a5a3a63eae11507", - "version" : "0.8.0" + "branch" : "main", + "revision" : "0856fdc2a7df229635c61dca36781f2001e5e254" } } ], diff --git a/navigation/build.gradle.kts b/navigation/build.gradle.kts index 555e8ff39..b1cac1edc 100644 --- a/navigation/build.gradle.kts +++ b/navigation/build.gradle.kts @@ -39,6 +39,9 @@ kotlin { implementation(projects.presentation.trailers) implementation(libs.kotlinInject.runtime) + + api(libs.decompose.decompose) + api(libs.essenty.lifecycle) } } } diff --git a/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/RootNavigationPresenter.kt b/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/RootNavigationPresenter.kt index 7c11e631f..e290ab4f9 100644 --- a/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/RootNavigationPresenter.kt +++ b/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/RootNavigationPresenter.kt @@ -19,14 +19,9 @@ import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsPresenterPre import com.thomaskioko.tvmaniac.presentation.trailers.TrailersPresenterFactory import com.thomaskioko.tvmaniac.presentation.watchlist.LibraryPresenterFactory import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager -import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.decompose.asValue import com.thomaskioko.tvmaniac.util.scope.ActivityScope -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.serialization.Serializable import me.tatarka.inject.annotations.Inject @@ -45,14 +40,11 @@ class RootNavigationPresenter( private val trailersPresenterFactory: TrailersPresenterFactory, private val traktAuthManager: TraktAuthManager, datastoreRepository: DatastoreRepository, - dispatchers: AppCoroutineDispatchers, ) : ComponentContext by componentContext { - private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.main) - private val navigation = StackNavigation() - internal val screenStack: Value> = + val screenStack: Value> = childStack( source = navigation, initialConfiguration = Config.Discover, @@ -61,17 +53,11 @@ class RootNavigationPresenter( childFactory = ::createScreen, ) - val state: StateFlow = datastoreRepository.observeTheme() - .map { theme -> - ThemeLoaded(theme = theme) - } - .stateIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = Loading, - ) + val state: Value = datastoreRepository.observeTheme() + .map { theme -> ThemeState(isFetching = false, appTheme = theme) } + .asValue(initialValue = ThemeState(), lifecycle = lifecycle) - internal fun bringToFront(config: Config) { + fun bringToFront(config: Config) { navigation.bringToFront(config) } @@ -149,7 +135,7 @@ class RootNavigationPresenter( } @Serializable - internal sealed interface Config { + sealed interface Config { @Serializable data object Discover : Config diff --git a/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/Screen.kt b/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/Screen.kt index 70019048b..b4ee2c1e9 100644 --- a/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/Screen.kt +++ b/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/Screen.kt @@ -9,7 +9,7 @@ import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsPresenter import com.thomaskioko.tvmaniac.presentation.trailers.TrailersPresenter import com.thomaskioko.tvmaniac.presentation.watchlist.LibraryPresenter -internal sealed interface Screen { +sealed interface Screen { class Discover(val presenter: DiscoverShowsPresenter) : Screen class Library(val presenter: LibraryPresenter) : Screen class MoreShows(val presenter: MoreShowsPresenter) : Screen diff --git a/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/ThemeState.kt b/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/ThemeState.kt index a112fb317..d457e8cc9 100644 --- a/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/ThemeState.kt +++ b/navigation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/ThemeState.kt @@ -1,10 +1,8 @@ package com.thomaskioko.tvmaniac.navigation -import com.thomaskioko.tvmaniac.datastore.api.Theme +import com.thomaskioko.tvmaniac.datastore.api.AppTheme -sealed interface ThemeState - -data object Loading : ThemeState -data class ThemeLoaded( - val theme: Theme = Theme.SYSTEM, -) : ThemeState +data class ThemeState( + val isFetching: Boolean = true, + val appTheme: AppTheme = AppTheme.SYSTEM_THEME, +) diff --git a/presentation/discover/build.gradle.kts b/presentation/discover/build.gradle.kts index dfbd9c9a4..d3140e9ce 100644 --- a/presentation/discover/build.gradle.kts +++ b/presentation/discover/build.gradle.kts @@ -13,6 +13,7 @@ kotlin { implementation(projects.data.shows.api) api(libs.decompose.decompose) + api(libs.essenty.lifecycle) api(libs.kotlinx.collections) implementation(libs.kotlinInject.runtime) diff --git a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsPresenter.kt b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsPresenter.kt index 53622ae5b..046073773 100644 --- a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsPresenter.kt +++ b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsPresenter.kt @@ -1,15 +1,13 @@ package com.thomaskioko.tvmaniac.presentation.discover import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.value.Value import com.thomaskioko.tvmaniac.category.api.model.Category import com.thomaskioko.tvmaniac.showimages.api.ShowImagesRepository import com.thomaskioko.tvmaniac.shows.api.DiscoverRepository -import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob +import com.thomaskioko.tvmaniac.util.decompose.asValue +import com.thomaskioko.tvmaniac.util.decompose.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -26,7 +24,6 @@ typealias DiscoverShowsPresenterFactory = ( @Inject class DiscoverShowsPresenter( - dispatchersProvider: AppCoroutineDispatchers, @Assisted componentContext: ComponentContext, @Assisted private val onNavigateToShowDetails: (Long) -> Unit, @Assisted private val onNavigateToMore: (Long) -> Unit, @@ -34,10 +31,11 @@ class DiscoverShowsPresenter( private val showImagesRepository: ShowImagesRepository, ) : ComponentContext by componentContext { - private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) + private val coroutineScope = coroutineScope() private val _state = MutableStateFlow(Loading) - val state: StateFlow = _state.asStateFlow() + val state: Value = _state + .asValue(initialValue = _state.value, lifecycle = lifecycle) init { coroutineScope.launch { diff --git a/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsPresenterTest.kt b/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsPresenterTest.kt index acd6fa785..182afa128 100644 --- a/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsPresenterTest.kt +++ b/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsPresenterTest.kt @@ -1,6 +1,5 @@ package com.thomaskioko.tvmaniac.presentation.discover -import app.cash.turbine.test import com.thomaskioko.tvmaniac.shows.testing.FakeDiscoverRepository import com.thomaskioko.tvmaniac.tmdb.testing.FakeShowImagesRepository import io.kotest.matchers.shouldBe @@ -43,9 +42,7 @@ internal class DiscoverShowsPresenterTest { discoverRepository.setShowCategory(categoryResult(3)) discoverRepository.setShowCategory(categoryResult(4)) - presenter.state.test { - awaitItem() shouldBe Loading - awaitItem() shouldBe discoverContent - } + presenter.state shouldBe Loading + presenter.state shouldBe discoverContent } } diff --git a/presentation/library/build.gradle.kts b/presentation/library/build.gradle.kts index 052fb96b7..68ba819dd 100644 --- a/presentation/library/build.gradle.kts +++ b/presentation/library/build.gradle.kts @@ -10,6 +10,7 @@ kotlin { implementation(projects.data.library.api) api(libs.decompose.decompose) + api(libs.essenty.lifecycle) api(libs.kotlinx.collections) implementation(libs.kotlinInject.runtime) diff --git a/presentation/library/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/LibraryAction.kt b/presentation/library/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/LibraryAction.kt index fa622e3d6..5985aaf25 100644 --- a/presentation/library/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/LibraryAction.kt +++ b/presentation/library/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/LibraryAction.kt @@ -3,4 +3,4 @@ package com.thomaskioko.tvmaniac.presentation.watchlist sealed interface LibraryAction data object ReloadLibrary : LibraryAction -data class ShowClicked(val id: Long) : LibraryAction +data class LibraryShowClicked(val id: Long) : LibraryAction diff --git a/presentation/library/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/LibraryPresenter.kt b/presentation/library/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/LibraryPresenter.kt index c6ad56bb7..6984b3f96 100644 --- a/presentation/library/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/LibraryPresenter.kt +++ b/presentation/library/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/LibraryPresenter.kt @@ -1,12 +1,11 @@ package com.thomaskioko.tvmaniac.presentation.watchlist import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.value.Value import com.thomaskioko.tvmaniac.shows.api.LibraryRepository -import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob +import com.thomaskioko.tvmaniac.util.decompose.asValue +import com.thomaskioko.tvmaniac.util.decompose.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -20,16 +19,16 @@ typealias LibraryPresenterFactory = ( @Inject class LibraryPresenter( - dispatchersProvider: AppCoroutineDispatchers, @Assisted componentContext: ComponentContext, @Assisted private val navigateToShowDetails: (id: Long) -> Unit, private val repository: LibraryRepository, ) : ComponentContext by componentContext { - private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) + private val coroutineScope = coroutineScope() private val _state = MutableStateFlow(LoadingShows) - val state = _state.asStateFlow() + val state: Value = _state + .asValue(initialValue = _state.value, lifecycle = lifecycle) init { fetchShowData() @@ -39,7 +38,7 @@ class LibraryPresenter( fun dispatch(action: LibraryAction) { when (action) { is ReloadLibrary -> coroutineScope.launch { fetchShowData() } - is ShowClicked -> navigateToShowDetails(action.id) + is LibraryShowClicked -> navigateToShowDetails(action.id) } } diff --git a/presentation/library/src/commonTest/kotlin/com/thomaskioko/tvmaniac/domain/watchlist/LibraryPresenterTest.kt b/presentation/library/src/commonTest/kotlin/com/thomaskioko/tvmaniac/domain/watchlist/LibraryPresenterTest.kt index b6f09bd96..aed0ce314 100644 --- a/presentation/library/src/commonTest/kotlin/com/thomaskioko/tvmaniac/domain/watchlist/LibraryPresenterTest.kt +++ b/presentation/library/src/commonTest/kotlin/com/thomaskioko/tvmaniac/domain/watchlist/LibraryPresenterTest.kt @@ -1,6 +1,5 @@ package com.thomaskioko.tvmaniac.domain.watchlist -import app.cash.turbine.test import com.thomaskioko.tvmaniac.presentation.watchlist.LibraryContent import com.thomaskioko.tvmaniac.presentation.watchlist.LibraryPresenter import com.thomaskioko.tvmaniac.presentation.watchlist.LoadingShows @@ -42,9 +41,7 @@ class LibraryPresenterTest { fun initial_state_emits_expected_result() = runTest { repository.setFollowedResult(watchlistResult) - presenter.state.test { - awaitItem() shouldBe LoadingShows - awaitItem() shouldBe LibraryContent(list = libraryItems) - } + presenter.state shouldBe LoadingShows + presenter.state shouldBe LibraryContent(list = libraryItems) } } diff --git a/presentation/more-shows/build.gradle.kts b/presentation/more-shows/build.gradle.kts index 2f52e6762..a5f7396e4 100644 --- a/presentation/more-shows/build.gradle.kts +++ b/presentation/more-shows/build.gradle.kts @@ -11,6 +11,7 @@ kotlin { implementation(projects.data.shows.api) api(libs.decompose.decompose) + api(libs.essenty.lifecycle) api(libs.kotlinx.collections) implementation(libs.kotlinInject.runtime) diff --git a/presentation/more-shows/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/moreshows/MoreShowsPresenter.kt b/presentation/more-shows/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/moreshows/MoreShowsPresenter.kt index f72bebd03..231ac0c2e 100644 --- a/presentation/more-shows/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/moreshows/MoreShowsPresenter.kt +++ b/presentation/more-shows/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/moreshows/MoreShowsPresenter.kt @@ -1,12 +1,10 @@ package com.thomaskioko.tvmaniac.presentation.moreshows import com.arkivanov.decompose.ComponentContext -import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob +import com.arkivanov.decompose.value.Value +import com.thomaskioko.tvmaniac.util.decompose.asValue +import com.thomaskioko.tvmaniac.util.decompose.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject @@ -19,17 +17,17 @@ typealias MoreShowsPresenterFactory = ( @Inject class MoreShowsPresenter( - dispatchersProvider: AppCoroutineDispatchers, @Assisted componentContext: ComponentContext, @Assisted categoryId: Long, @Assisted onBack: () -> Unit, @Assisted private val onNavigateToShowDetails: (Long) -> Unit, -) { +) : ComponentContext by componentContext { - private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) + private val coroutineScope = coroutineScope() private val _state = MutableStateFlow(MoreShowsState()) - val state: StateFlow = _state.asStateFlow() + val state: Value = _state + .asValue(initialValue = _state.value, lifecycle = lifecycle) fun dispatch(action: MoreShowsActions) { } diff --git a/presentation/profile/build.gradle.kts b/presentation/profile/build.gradle.kts index ac9d971c4..36f8207fc 100644 --- a/presentation/profile/build.gradle.kts +++ b/presentation/profile/build.gradle.kts @@ -12,6 +12,7 @@ kotlin { implementation(projects.data.profilestats.api) api(libs.decompose.decompose) + api(libs.essenty.lifecycle) api(libs.kotlinx.collections) implementation(libs.kotlinInject.runtime) diff --git a/presentation/profile/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfilePresenter.kt b/presentation/profile/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfilePresenter.kt index 04ab0fd05..927db54f3 100644 --- a/presentation/profile/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfilePresenter.kt +++ b/presentation/profile/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfilePresenter.kt @@ -1,15 +1,13 @@ package com.thomaskioko.tvmaniac.presentation.profile import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.value.Value import com.thomaskioko.tvmaniac.datastore.api.DatastoreRepository import com.thomaskioko.tvmaniac.profile.api.ProfileRepository import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthRepository -import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob +import com.thomaskioko.tvmaniac.util.decompose.asValue +import com.thomaskioko.tvmaniac.util.decompose.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -25,7 +23,6 @@ typealias ProfilePresenterFactory = ( @Inject class ProfilePresenter( - dispatchersProvider: AppCoroutineDispatchers, @Assisted componentContext: ComponentContext, @Assisted private val navigateToSettings: () -> Unit, @Assisted private val launchTraktWebView: () -> Unit, @@ -34,10 +31,11 @@ class ProfilePresenter( private val profileRepository: ProfileRepository, ) : ComponentContext by componentContext { - private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) + private val coroutineScope = coroutineScope() private val _state: MutableStateFlow = MutableStateFlow(ProfileState()) - val state: StateFlow = _state.asStateFlow() + val state: Value = _state + .asValue(initialValue = _state.value, lifecycle = lifecycle) init { observeAuthState() diff --git a/presentation/profile/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfilePresenterTest.kt b/presentation/profile/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfilePresenterTest.kt index 10e3bafae..203389a44 100644 --- a/presentation/profile/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfilePresenterTest.kt +++ b/presentation/profile/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfilePresenterTest.kt @@ -1,6 +1,5 @@ package com.thomaskioko.tvmaniac.presentation.profile -import app.cash.turbine.test import com.thomaskioko.tvmaniac.datastore.testing.FakeDatastoreRepository import com.thomaskioko.tvmaniac.datastore.testing.authenticatedAuthState import com.thomaskioko.tvmaniac.trakt.profile.testing.FakeProfileRepository @@ -35,11 +34,11 @@ class ProfilePresenterTest { @BeforeTest fun setUp() { Dispatchers.setMain(testDispatcher) - /* presenter = ProfilePresenter( - datastoreRepository = datastoreRepository, - profileRepository = profileRepository, - traktAuthRepository = traktAuthRepository, - )*/ + /* presenter = ProfilePresenter( + datastoreRepository = datastoreRepository, + profileRepository = profileRepository, + traktAuthRepository = traktAuthRepository, + )*/ } @AfterTest @@ -49,96 +48,88 @@ class ProfilePresenterTest { @Test fun initial_state_emits_expected_result() = runTest { - presenter.state.test { - awaitItem() shouldBe ProfileState() - } + presenter.state shouldBe ProfileState() } @Test fun given_ShowTraktDialog_andUserIsAuthenticated_expectedResultIsEmitted() = runTest { - presenter.state.test { - awaitItem() shouldBe ProfileState() // Initial State - - presenter.dispatch(ShowTraktDialog) - - awaitItem() shouldBe ProfileState() - .copy(showTraktDialog = true) - - presenter.dispatch(TraktLoginClicked) - - awaitItem() shouldBe ProfileState() - .copy(showTraktDialog = false) - - traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) - datastoreRepository.setAuthState(authenticatedAuthState) - profileRepository.setUserData(Either.Right(user)) - - awaitItem() shouldBe ProfileState() - .copy( - errorMessage = null, - userInfo = UserInfo( - slug = user.slug, - userName = user.user_name, - fullName = user.full_name, - userPicUrl = user.profile_picture, - ), - ) - } + presenter.state shouldBe ProfileState() // Initial State + + presenter.dispatch(ShowTraktDialog) + + presenter.state shouldBe ProfileState() + .copy(showTraktDialog = true) + + presenter.dispatch(TraktLoginClicked) + + presenter.state shouldBe ProfileState() + .copy(showTraktDialog = false) + + traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) + datastoreRepository.setAuthState(authenticatedAuthState) + profileRepository.setUserData(Either.Right(user)) + + presenter.state shouldBe ProfileState() + .copy( + errorMessage = null, + userInfo = UserInfo( + slug = user.slug, + userName = user.user_name, + fullName = user.full_name, + userPicUrl = user.profile_picture, + ), + ) } @Test fun given_TraktLoginClicked_andErrorOccurs_expectedResultIsEmitted() = runTest { - presenter.state.test { - val errorMessage = "Something happened" + val errorMessage = "Something happened" - awaitItem() shouldBe ProfileState() + presenter.state shouldBe ProfileState() - presenter.dispatch(ShowTraktDialog) + presenter.dispatch(ShowTraktDialog) - awaitItem() shouldBe ProfileState().copy( - showTraktDialog = true, - ) + presenter.state shouldBe ProfileState().copy( + showTraktDialog = true, + ) - presenter.dispatch(TraktLoginClicked) + presenter.dispatch(TraktLoginClicked) - awaitItem() shouldBe ProfileState().copy( - showTraktDialog = false, - ) + presenter.state shouldBe ProfileState().copy( + showTraktDialog = false, + ) - traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) - datastoreRepository.setAuthState(authenticatedAuthState) - profileRepository.setUserData( - Either.Left(ServerError(errorMessage)), - ) + traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) + datastoreRepository.setAuthState(authenticatedAuthState) + profileRepository.setUserData( + Either.Left(ServerError(errorMessage)), + ) - awaitItem() shouldBe ProfileState() - .copy(errorMessage = errorMessage) - } + presenter.state shouldBe ProfileState() + .copy(errorMessage = errorMessage) } @Test fun given_TraktLogoutClicked_expectedResultIsEmitted() = runTest { - presenter.state.test { - awaitItem() shouldBe ProfileState() - - traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) - datastoreRepository.setAuthState(authenticatedAuthState) - profileRepository.setUserData(Either.Right(user)) - - awaitItem() shouldBe ProfileState() - .copy( - errorMessage = null, - userInfo = UserInfo( - slug = user.slug, - userName = user.user_name, - fullName = user.full_name, - userPicUrl = user.profile_picture, - ), - ) - - presenter.dispatch(TraktLogoutClicked) - - awaitItem() shouldBe ProfileState() - } + presenter.state shouldBe ProfileState() + + traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) + datastoreRepository.setAuthState(authenticatedAuthState) + profileRepository.setUserData(Either.Right(user)) + + presenter.state shouldBe ProfileState() + .copy( + errorMessage = null, + userInfo = UserInfo( + slug = user.slug, + userName = user.user_name, + fullName = user.full_name, + userPicUrl = user.profile_picture, + ), + ) + + presenter.dispatch(TraktLogoutClicked) + + presenter.state shouldBe ProfileState() } } diff --git a/presentation/search/build.gradle.kts b/presentation/search/build.gradle.kts index 2dafd26f3..3aa752993 100644 --- a/presentation/search/build.gradle.kts +++ b/presentation/search/build.gradle.kts @@ -6,8 +6,10 @@ kotlin { sourceSets { commonMain { dependencies { + implementation(projects.core.util) api(libs.decompose.decompose) + api(libs.essenty.lifecycle) implementation(libs.kotlinInject.runtime) } } diff --git a/presentation/search/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/search/SearchPresenter.kt b/presentation/search/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/search/SearchPresenter.kt index 0b5d843b4..de36f6616 100644 --- a/presentation/search/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/search/SearchPresenter.kt +++ b/presentation/search/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/search/SearchPresenter.kt @@ -1,6 +1,10 @@ package com.thomaskioko.tvmaniac.presentation.search import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.value.Value +import com.thomaskioko.tvmaniac.util.decompose.asValue +import com.thomaskioko.tvmaniac.util.decompose.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject @@ -13,4 +17,11 @@ typealias SearchPresenterFactory = ( class SearchPresenter( @Assisted componentContext: ComponentContext, @Assisted goBack: () -> Unit, -) : ComponentContext by componentContext +) : ComponentContext by componentContext { + + private val coroutineScope = coroutineScope() + private val _state: MutableStateFlow = MutableStateFlow(SearchLoading) + + val state: Value = _state + .asValue(initialValue = _state.value, lifecycle = lifecycle) +} diff --git a/presentation/search/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/search/SearchState.kt b/presentation/search/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/search/SearchState.kt new file mode 100644 index 000000000..04058c485 --- /dev/null +++ b/presentation/search/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/search/SearchState.kt @@ -0,0 +1,5 @@ +package com.thomaskioko.tvmaniac.presentation.search + +interface SearchState + +data object SearchLoading : SearchState diff --git a/presentation/seasondetails/build.gradle.kts b/presentation/seasondetails/build.gradle.kts index 315974099..0d06fdd9a 100644 --- a/presentation/seasondetails/build.gradle.kts +++ b/presentation/seasondetails/build.gradle.kts @@ -12,6 +12,7 @@ kotlin { implementation(projects.data.seasondetails.api) api(libs.decompose.decompose) + api(libs.essenty.lifecycle) api(libs.kotlinx.collections) implementation(libs.kotlinInject.runtime) diff --git a/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsPresenter.kt b/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsPresenter.kt index a6e48a67f..c00474f88 100644 --- a/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsPresenter.kt +++ b/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsPresenter.kt @@ -1,14 +1,12 @@ package com.thomaskioko.tvmaniac.presentation.seasondetails import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.value.Value import com.thomaskioko.tvmaniac.episodeimages.api.EpisodeImageRepository import com.thomaskioko.tvmaniac.seasondetails.api.SeasonDetailsRepository -import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob +import com.thomaskioko.tvmaniac.util.decompose.asValue +import com.thomaskioko.tvmaniac.util.decompose.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update @@ -25,7 +23,6 @@ typealias SeasonDetailsPresenterFactory = ( ) -> SeasonDetailsPresenter class SeasonDetailsPresenter @Inject constructor( - dispatchersProvider: AppCoroutineDispatchers, @Assisted componentContext: ComponentContext, @Assisted private val traktId: Long, @Assisted private val title: String?, @@ -35,9 +32,10 @@ class SeasonDetailsPresenter @Inject constructor( private val episodeImageRepository: EpisodeImageRepository, ) : ComponentContext by componentContext { - private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) + private val coroutineScope = coroutineScope() private val _state = MutableStateFlow(Loading) - val state: StateFlow = _state.asStateFlow() + val state: Value = _state + .asValue(initialValue = _state.value, lifecycle = lifecycle) init { coroutineScope.launch { diff --git a/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/SeasonPresenterTest.kt b/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/SeasonPresenterTest.kt index 09d669f01..0c34b944f 100644 --- a/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/SeasonPresenterTest.kt +++ b/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/SeasonPresenterTest.kt @@ -1,6 +1,5 @@ package com.thomaskioko.tvmaniac.data.seasondetails -import app.cash.turbine.test import com.thomaskioko.tvmaniac.episodes.testing.FakeEpisodeImageRepository import com.thomaskioko.tvmaniac.presentation.seasondetails.Loading import com.thomaskioko.tvmaniac.presentation.seasondetails.SeasonDetailsPresenter @@ -34,11 +33,11 @@ class SeasonPresenterTest { @BeforeTest fun setUp() { Dispatchers.setMain(testDispatcher) - /* presenter = SeasonDetailsPresenter( - traktId = 1231, - episodeImageRepository = episodeImageRepository, - seasonDetailsRepository = seasonDetailsRepository, - )*/ + /* presenter = SeasonDetailsPresenter( + traktId = 1231, + episodeImageRepository = episodeImageRepository, + seasonDetailsRepository = seasonDetailsRepository, + )*/ } @AfterTest @@ -48,25 +47,21 @@ class SeasonPresenterTest { @Test fun onLoadSeasonDetails_correct_state_is_emitted() = runTest { - presenter.state.test { - seasonDetailsRepository.setCachedResults(SeasonWithEpisodeList) + seasonDetailsRepository.setCachedResults(SeasonWithEpisodeList) - awaitItem() shouldBe Loading - awaitItem() shouldBe seasonDetailsLoaded - } + presenter.state shouldBe Loading + presenter.state shouldBe seasonDetailsLoaded } @Test fun onLoadSeasonDetails_andErrorOccurs_correctStateIsEmitted() = runTest { - presenter.state.test { - val errorMessage = "Something went wrong" - seasonDetailsRepository.setCachedResults(SeasonWithEpisodeList) - seasonDetailsRepository.setSeasonsResult(Either.Left(DefaultError(errorMessage))) + val errorMessage = "Something went wrong" + seasonDetailsRepository.setCachedResults(SeasonWithEpisodeList) + seasonDetailsRepository.setSeasonsResult(Either.Left(DefaultError(errorMessage))) - awaitItem() shouldBe Loading - awaitItem() shouldBe seasonDetailsLoaded - awaitItem() shouldBe seasonDetailsLoaded - .copy(errorMessage = errorMessage) - } + presenter.state shouldBe Loading + presenter.state shouldBe seasonDetailsLoaded + presenter.state shouldBe seasonDetailsLoaded + .copy(errorMessage = errorMessage) } } diff --git a/presentation/settings/build.gradle.kts b/presentation/settings/build.gradle.kts index 6d84075dd..a3220d6a8 100644 --- a/presentation/settings/build.gradle.kts +++ b/presentation/settings/build.gradle.kts @@ -11,6 +11,7 @@ kotlin { implementation(projects.core.traktAuth.api) api(libs.decompose.decompose) + api(libs.essenty.lifecycle) implementation(libs.kotlinInject.runtime) } diff --git a/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsActions.kt b/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsActions.kt index d8e09003c..7013e7fb1 100644 --- a/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsActions.kt +++ b/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsActions.kt @@ -1,10 +1,10 @@ package com.thomaskioko.tvmaniac.presentation.settings -import com.thomaskioko.tvmaniac.datastore.api.Theme +import com.thomaskioko.tvmaniac.datastore.api.AppTheme sealed class SettingsActions data class ThemeSelected( - val theme: Theme, + val appTheme: AppTheme, ) : SettingsActions() data object ChangeThemeClicked : SettingsActions() diff --git a/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsPresenter.kt b/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsPresenter.kt index de71ce05a..0a7ca4fce 100644 --- a/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsPresenter.kt +++ b/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsPresenter.kt @@ -1,16 +1,14 @@ package com.thomaskioko.tvmaniac.presentation.settings import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.value.Value import com.thomaskioko.tvmaniac.datastore.api.DatastoreRepository import com.thomaskioko.tvmaniac.profile.api.ProfileRepository import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthRepository import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthState -import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob +import com.thomaskioko.tvmaniac.util.decompose.asValue +import com.thomaskioko.tvmaniac.util.decompose.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -24,7 +22,6 @@ typealias SettingsPresenterFactory = ( @Inject class SettingsPresenter( - dispatchersProvider: AppCoroutineDispatchers, @Assisted componentContext: ComponentContext, @Assisted private val launchWebView: () -> Unit, private val datastoreRepository: DatastoreRepository, @@ -32,11 +29,12 @@ class SettingsPresenter( private val traktAuthRepository: TraktAuthRepository, ) : ComponentContext by componentContext { - private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) + private val coroutineScope = coroutineScope() private val _state: MutableStateFlow = MutableStateFlow(SettingsState.DEFAULT_STATE) - val state: StateFlow = _state.asStateFlow() + val state: Value = _state + .asValue(initialValue = _state.value, lifecycle = lifecycle) init { coroutineScope.launch { @@ -53,7 +51,7 @@ class SettingsPresenter( DismissTraktDialog -> updateTrackDialogState(false) ShowTraktDialog -> updateTrackDialogState(true) is ThemeSelected -> { - datastoreRepository.saveTheme(action.theme) + datastoreRepository.saveTheme(action.appTheme) updateThemeDialogState(false) } @@ -99,7 +97,7 @@ class SettingsPresenter( datastoreRepository.observeTheme() .collectLatest { _state.update { state -> - state.copy(theme = it) + state.copy(appTheme = it) } } } diff --git a/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsState.kt b/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsState.kt index 59a43b13e..26d640ca6 100644 --- a/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsState.kt +++ b/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsState.kt @@ -1,10 +1,10 @@ package com.thomaskioko.tvmaniac.presentation.settings -import com.thomaskioko.tvmaniac.datastore.api.Theme +import com.thomaskioko.tvmaniac.datastore.api.AppTheme data class SettingsState( val userInfo: UserInfo?, - val theme: Theme, + val appTheme: AppTheme, val showTraktDialog: Boolean, val showthemePopup: Boolean, val errorMessage: String?, @@ -14,7 +14,7 @@ data class SettingsState( companion object { val DEFAULT_STATE = SettingsState( userInfo = null, - theme = Theme.SYSTEM, + appTheme = AppTheme.SYSTEM_THEME, showTraktDialog = false, showthemePopup = false, isLoading = false, diff --git a/presentation/settings/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsPresenterTest.kt b/presentation/settings/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsPresenterTest.kt index 7e96711af..54f25e031 100644 --- a/presentation/settings/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsPresenterTest.kt +++ b/presentation/settings/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsPresenterTest.kt @@ -1,7 +1,6 @@ package com.thomaskioko.tvmaniac.presentation.settings -import app.cash.turbine.test -import com.thomaskioko.tvmaniac.datastore.api.Theme +import com.thomaskioko.tvmaniac.datastore.api.AppTheme import com.thomaskioko.tvmaniac.datastore.testing.FakeDatastoreRepository import com.thomaskioko.tvmaniac.datastore.testing.authenticatedAuthState import com.thomaskioko.tvmaniac.trakt.profile.testing.FakeProfileRepository @@ -37,11 +36,11 @@ class SettingsPresenterTest { @BeforeTest fun setUp() { Dispatchers.setMain(testDispatcher) - /* screenModel = SettingsPresenter( - datastoreRepository = datastoreRepository, - profileRepository = profileRepository, - traktAuthRepository = traktAuthRepository, - )*/ + /* screenModel = SettingsPresenter( + datastoreRepository = datastoreRepository, + profileRepository = profileRepository, + traktAuthRepository = traktAuthRepository, + )*/ } @AfterTest @@ -51,161 +50,147 @@ class SettingsPresenterTest { @Test fun initial_state_emits_expected_result() = runTest { - presenter.state.test { - awaitItem() shouldBe SettingsState.DEFAULT_STATE - } + presenter.state shouldBe SettingsState.DEFAULT_STATE } @Test fun when_theme_is_updated_expected_result_is_emitted() = runTest { - presenter.state.test { - awaitItem() shouldBe SettingsState.DEFAULT_STATE // Initial State - - presenter.dispatch(ChangeThemeClicked) - awaitItem() shouldBe SettingsState.DEFAULT_STATE.copy( - showthemePopup = true, - ) - - datastoreRepository.setTheme(Theme.DARK) - presenter.dispatch(ThemeSelected(Theme.DARK)) - - awaitItem() shouldBe SettingsState.DEFAULT_STATE.copy( - showthemePopup = true, - theme = Theme.DARK, - ) - awaitItem() shouldBe SettingsState.DEFAULT_STATE.copy( - showthemePopup = false, - theme = Theme.DARK, - ) - } + presenter.state shouldBe SettingsState.DEFAULT_STATE // Initial State + + presenter.dispatch(ChangeThemeClicked) + presenter.state shouldBe SettingsState.DEFAULT_STATE.copy( + showthemePopup = true, + ) + + datastoreRepository.setTheme(AppTheme.DARK_THEME) + presenter.dispatch(ThemeSelected(AppTheme.DARK_THEME)) + + presenter.state shouldBe SettingsState.DEFAULT_STATE.copy( + showthemePopup = true, + appTheme = AppTheme.DARK_THEME, + ) + presenter.state shouldBe SettingsState.DEFAULT_STATE.copy( + showthemePopup = false, + appTheme = AppTheme.DARK_THEME, + ) } @Test fun when_dialog_is_dismissed_expected_result_is_emitted() = runTest { - presenter.state.test { - awaitItem() shouldBe SettingsState.DEFAULT_STATE // Initial State + presenter.state shouldBe SettingsState.DEFAULT_STATE // Initial State - presenter.dispatch(ChangeThemeClicked) + presenter.dispatch(ChangeThemeClicked) - awaitItem() shouldBe SettingsState.DEFAULT_STATE.copy( - showthemePopup = true, - ) + presenter.state shouldBe SettingsState.DEFAULT_STATE.copy( + showthemePopup = true, + ) - presenter.dispatch(DismissThemeClicked) + presenter.dispatch(DismissThemeClicked) - awaitItem() shouldBe SettingsState.DEFAULT_STATE.copy( - showthemePopup = false, - ) - } + presenter.state shouldBe SettingsState.DEFAULT_STATE.copy( + showthemePopup = false, + ) } @Test fun when_ShowTraktDialog_is_clicked_expected_result_is_emitted() = runTest { - presenter.state.test { - awaitItem() shouldBe SettingsState.DEFAULT_STATE // Initial State + presenter.state shouldBe SettingsState.DEFAULT_STATE // Initial State - presenter.dispatch(ShowTraktDialog) + presenter.dispatch(ShowTraktDialog) - awaitItem() shouldBe SettingsState.DEFAULT_STATE.copy( - showTraktDialog = true, - ) + presenter.state shouldBe SettingsState.DEFAULT_STATE.copy( + showTraktDialog = true, + ) - presenter.dispatch(DismissTraktDialog) + presenter.dispatch(DismissTraktDialog) - awaitItem() shouldBe SettingsState.DEFAULT_STATE.copy( - showTraktDialog = false, - ) - } + presenter.state shouldBe SettingsState.DEFAULT_STATE.copy( + showTraktDialog = false, + ) } @Ignore // "Fix once TraktAuthManager is implemented" @Test fun given_TraktLoginClicked_andUserIsAuthenticated_expectedResultIsEmitted() = runTest { - presenter.state.test { - awaitItem() shouldBe SettingsState.DEFAULT_STATE // Initial State - - presenter.dispatch(ShowTraktDialog) - - awaitItem() shouldBe SettingsState.DEFAULT_STATE - .copy(showTraktDialog = true) - - presenter.dispatch(TraktLoginClicked) - - awaitItem() shouldBe SettingsState.DEFAULT_STATE - .copy(showTraktDialog = false) - - traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) - datastoreRepository.setAuthState(authenticatedAuthState) - profileRepository.setUserData(Either.Right(user)) - - awaitItem() shouldBe SettingsState.DEFAULT_STATE - .copy( - errorMessage = null, - userInfo = UserInfo( - slug = user.slug, - userName = user.user_name, - fullName = user.full_name, - userPicUrl = user.profile_picture, - ), - ) - } + presenter.state shouldBe SettingsState.DEFAULT_STATE // Initial State + + presenter.dispatch(ShowTraktDialog) + + presenter.state shouldBe SettingsState.DEFAULT_STATE + .copy(showTraktDialog = true) + + presenter.dispatch(TraktLoginClicked) + + presenter.state shouldBe SettingsState.DEFAULT_STATE + .copy(showTraktDialog = false) + + traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) + datastoreRepository.setAuthState(authenticatedAuthState) + profileRepository.setUserData(Either.Right(user)) + + presenter.state shouldBe SettingsState.DEFAULT_STATE + .copy( + errorMessage = null, + userInfo = UserInfo( + slug = user.slug, + userName = user.user_name, + fullName = user.full_name, + userPicUrl = user.profile_picture, + ), + ) } @Ignore // "Fix once TraktAuthManager is implemented" @Test fun given_TraktLoginClicked_andErrorOccurs_expectedResultIsEmitted() = runTest { - presenter.state.test { - val errorMessage = "Something happened" + val errorMessage = "Something happened" - awaitItem() shouldBe SettingsState.DEFAULT_STATE // Initial State + presenter.state shouldBe SettingsState.DEFAULT_STATE // Initial State - presenter.dispatch(ShowTraktDialog) + presenter.dispatch(ShowTraktDialog) - awaitItem() shouldBe SettingsState.DEFAULT_STATE - .copy(showTraktDialog = true) + presenter.state shouldBe SettingsState.DEFAULT_STATE + .copy(showTraktDialog = true) - presenter.dispatch(TraktLoginClicked) + presenter.dispatch(TraktLoginClicked) - awaitItem() shouldBe SettingsState.DEFAULT_STATE - .copy(showTraktDialog = false) + presenter.state shouldBe SettingsState.DEFAULT_STATE + .copy(showTraktDialog = false) - traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) - datastoreRepository.setAuthState(authenticatedAuthState) - profileRepository.setUserData(Either.Left(ServerError(errorMessage))) + traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) + datastoreRepository.setAuthState(authenticatedAuthState) + profileRepository.setUserData(Either.Left(ServerError(errorMessage))) - awaitItem() shouldBe SettingsState.DEFAULT_STATE - .copy(errorMessage = errorMessage) - } + presenter.state shouldBe SettingsState.DEFAULT_STATE + .copy(errorMessage = errorMessage) } @Ignore // "Fix once TraktAuthManager is implemented" @Test fun given_TraktLogoutClicked_expectedResultIsEmitted() = runTest { - presenter.state.test { - awaitItem() shouldBe SettingsState.DEFAULT_STATE // Initial State + presenter.state shouldBe SettingsState.DEFAULT_STATE // Initial State - presenter.dispatch(ShowTraktDialog) + presenter.dispatch(ShowTraktDialog) - awaitItem() shouldBe SettingsState.DEFAULT_STATE - .copy(showTraktDialog = true) + presenter.state shouldBe SettingsState.DEFAULT_STATE + .copy(showTraktDialog = true) - presenter.dispatch(TraktLoginClicked) + presenter.dispatch(TraktLoginClicked) - traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) - datastoreRepository.setAuthState(authenticatedAuthState) - profileRepository.setUserData(Either.Right(user)) + traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) + datastoreRepository.setAuthState(authenticatedAuthState) + profileRepository.setUserData(Either.Right(user)) - awaitItem() shouldBe SettingsState.DEFAULT_STATE - .copy( - errorMessage = null, - userInfo = null, - ) + presenter.state shouldBe SettingsState.DEFAULT_STATE + .copy( + errorMessage = null, + userInfo = null, + ) - presenter.dispatch(TraktLogoutClicked) + presenter.dispatch(TraktLogoutClicked) - traktAuthRepository.setAuthState(TraktAuthState.LOGGED_OUT) + traktAuthRepository.setAuthState(TraktAuthState.LOGGED_OUT) - awaitItem() shouldBe SettingsState.DEFAULT_STATE - } + presenter.state shouldBe SettingsState.DEFAULT_STATE } } diff --git a/presentation/show-details/build.gradle.kts b/presentation/show-details/build.gradle.kts index ce48fa69f..bb88a4a17 100644 --- a/presentation/show-details/build.gradle.kts +++ b/presentation/show-details/build.gradle.kts @@ -14,6 +14,7 @@ kotlin { implementation(projects.data.library.api) api(libs.decompose.decompose) + api(libs.essenty.lifecycle) api(libs.kotlinx.collections) implementation(libs.kotlinInject.runtime) diff --git a/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsAction.kt b/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsAction.kt index eacb0b680..db13d4203 100644 --- a/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsAction.kt +++ b/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsAction.kt @@ -4,13 +4,10 @@ sealed interface ShowDetailsAction data object WebViewError : ShowDetailsAction data object DismissWebViewError : ShowDetailsAction -data object BackClicked : ShowDetailsAction +data object DetailBackClicked : ShowDetailsAction data class SeasonClicked(val id: Long, val title: String) : ShowDetailsAction -data class ShowClicked(val id: Long) : ShowDetailsAction +data class DetailShowClicked(val id: Long) : ShowDetailsAction data class WatchTrailerClicked(val id: Long) : ShowDetailsAction data class ReloadShowDetails(val traktId: Long) : ShowDetailsAction - -data class FollowShowClicked( - val addToLibrary: Boolean, -) : ShowDetailsAction +data class FollowShowClicked(val addToLibrary: Boolean) : ShowDetailsAction diff --git a/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsPresenter.kt b/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsPresenter.kt index 69f8c797a..f36f4ed33 100644 --- a/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsPresenter.kt +++ b/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsPresenter.kt @@ -1,6 +1,7 @@ package com.thomaskioko.tvmaniac.presentation.showdetails import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.value.Value import com.thomaskioko.tvmaniac.core.db.SeasonsByShowId import com.thomaskioko.tvmaniac.core.db.ShowById import com.thomaskioko.tvmaniac.core.db.SimilarShows @@ -11,14 +12,11 @@ import com.thomaskioko.tvmaniac.seasons.api.SeasonsRepository import com.thomaskioko.tvmaniac.shows.api.DiscoverRepository import com.thomaskioko.tvmaniac.shows.api.LibraryRepository import com.thomaskioko.tvmaniac.similar.api.SimilarShowsRepository -import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.decompose.asValue +import com.thomaskioko.tvmaniac.util.decompose.coroutineScope import com.thomaskioko.tvmaniac.util.model.Either import com.thomaskioko.tvmaniac.util.model.Failure -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update @@ -36,7 +34,6 @@ typealias ShowDetailsPresenterPresenterFactory = ( ) -> ShowDetailsPresenter class ShowDetailsPresenter @Inject constructor( - dispatchersProvider: AppCoroutineDispatchers, @Assisted componentContext: ComponentContext, @Assisted private val traktShowId: Long, @Assisted private val onBack: () -> Unit, @@ -50,9 +47,10 @@ class ShowDetailsPresenter @Inject constructor( private val libraryRepository: LibraryRepository, ) : ComponentContext by componentContext { - private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) + private val coroutineScope = coroutineScope() private val _state = MutableStateFlow(ShowDetailsState.EMPTY_DETAIL_STATE) - val state: StateFlow = _state.asStateFlow() + val state: Value = _state + .asValue(initialValue = _state.value, lifecycle = lifecycle) init { coroutineScope.launch { @@ -63,9 +61,9 @@ class ShowDetailsPresenter @Inject constructor( fun dispatch(action: ShowDetailsAction) { when (action) { - BackClicked -> onBack() + DetailBackClicked -> onBack() is SeasonClicked -> onNavigateToSeason(action.id, action.title) - is ShowClicked -> onNavigateToShow(action.id) + is DetailShowClicked -> onNavigateToShow(action.id) is WatchTrailerClicked -> onNavigateToTrailer(action.id) DismissWebViewError -> { coroutineScope.launch { diff --git a/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsPresenterTest.kt b/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsPresenterTest.kt index 970a8cbe1..d0cab815f 100644 --- a/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsPresenterTest.kt +++ b/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsPresenterTest.kt @@ -1,6 +1,5 @@ package com.thomaskioko.tvmaniac.presentation.showdetails -import app.cash.turbine.test import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsState.Companion.EMPTY_DETAIL_STATE import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsState.SimilarShowsContent.Companion.EMPTY_SIMILAR_SHOWS import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsState.TrailersContent.Companion.EMPTY_TRAILERS @@ -41,14 +40,14 @@ internal class ShowDetailsPresenterTest { @BeforeTest fun setUp() { Dispatchers.setMain(testDispatcher) - /* presenter = ShowDetailsPresenter( - traktShowId = 84958, - discoverRepository = discoverRepository, - trailerRepository = trailerRepository, - seasonsRepository = seasonsRepository, - similarShowsRepository = similarShowsRepository, - libraryRepository = fakeLibraryRepository, - )*/ + /* presenter = ShowDetailsPresenter( + traktShowId = 84958, + discoverRepository = discoverRepository, + trailerRepository = trailerRepository, + seasonsRepository = seasonsRepository, + similarShowsRepository = similarShowsRepository, + libraryRepository = fakeLibraryRepository, + )*/ } @AfterTest @@ -58,142 +57,130 @@ internal class ShowDetailsPresenterTest { @Test fun initial_state_emits_expected_result() = runTest { - presenter.state.test { - discoverRepository.setShowById(selectedShow) + discoverRepository.setShowById(selectedShow) - awaitItem() shouldBe EMPTY_DETAIL_STATE.copy( - show = show, - ) - } + presenter.state shouldBe EMPTY_DETAIL_STATE.copy( + show = show, + ) } @Test fun loadingData_state_emits_expected_result() = runTest { - presenter.state.test { - discoverRepository.setShowResult(Either.Right(selectedShow)) - seasonsRepository.setSeasonsResult(Either.Right(seasons)) - similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) - trailerRepository.setTrailerResult(Either.Right(trailers)) - - awaitItem() shouldBe EMPTY_DETAIL_STATE - awaitItem() shouldBe showDetailsLoaded - awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = seasonsShowDetailsLoaded, - ) - awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = seasonsShowDetailsLoaded, - trailersContent = trailerShowDetailsLoaded, - ) - awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = seasonsShowDetailsLoaded, - trailersContent = trailerShowDetailsLoaded, - similarShowsContent = similarShowLoaded, - ) - } + discoverRepository.setShowResult(Either.Right(selectedShow)) + seasonsRepository.setSeasonsResult(Either.Right(seasons)) + similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) + trailerRepository.setTrailerResult(Either.Right(trailers)) + + presenter.state shouldBe EMPTY_DETAIL_STATE + presenter.state shouldBe showDetailsLoaded + presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = seasonsShowDetailsLoaded, + ) + presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = seasonsShowDetailsLoaded, + trailersContent = trailerShowDetailsLoaded, + ) + presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = seasonsShowDetailsLoaded, + trailersContent = trailerShowDetailsLoaded, + similarShowsContent = similarShowLoaded, + ) } @Test fun error_loading_similarShows_emits_expected_result() = runTest { - presenter.state.test { - val errorMessage = "Something went wrong" - discoverRepository.setShowResult(Either.Right(selectedShow)) - seasonsRepository.setSeasonsResult(Either.Right(seasons)) - trailerRepository.setTrailerResult(Either.Right(trailers)) - similarShowsRepository.setSimilarShowsResult(Either.Left(ServerError(errorMessage))) - - awaitItem() shouldBe EMPTY_DETAIL_STATE - awaitItem() shouldBe showDetailsLoaded - awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = seasonsShowDetailsLoaded, - ) - awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = seasonsShowDetailsLoaded, - trailersContent = trailerShowDetailsLoaded, - ) - awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = seasonsShowDetailsLoaded, - trailersContent = trailerShowDetailsLoaded, - similarShowsContent = EMPTY_SIMILAR_SHOWS.copy( - errorMessage = errorMessage, - ), - ) - } + val errorMessage = "Something went wrong" + discoverRepository.setShowResult(Either.Right(selectedShow)) + seasonsRepository.setSeasonsResult(Either.Right(seasons)) + trailerRepository.setTrailerResult(Either.Right(trailers)) + similarShowsRepository.setSimilarShowsResult(Either.Left(ServerError(errorMessage))) + + presenter.state shouldBe EMPTY_DETAIL_STATE + presenter.state shouldBe showDetailsLoaded + presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = seasonsShowDetailsLoaded, + ) + presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = seasonsShowDetailsLoaded, + trailersContent = trailerShowDetailsLoaded, + ) + presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = seasonsShowDetailsLoaded, + trailersContent = trailerShowDetailsLoaded, + similarShowsContent = EMPTY_SIMILAR_SHOWS.copy( + errorMessage = errorMessage, + ), + ) } @Test fun error_loading_trailers_emits_expected_result() = runTest { - presenter.state.test { - val errorMessage = "Something went wrong" - discoverRepository.setShowResult(Either.Right(selectedShow)) - seasonsRepository.setSeasonsResult(Either.Right(seasons)) - similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) - trailerRepository.setTrailerResult(Either.Left(ServerError(errorMessage))) - - awaitItem() shouldBe EMPTY_DETAIL_STATE - awaitItem() shouldBe showDetailsLoaded - awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = seasonsShowDetailsLoaded, - ) - awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = seasonsShowDetailsLoaded, - trailersContent = EMPTY_TRAILERS.copy( - errorMessage = errorMessage, - ), - ) - awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = seasonsShowDetailsLoaded, - similarShowsContent = similarShowLoaded, - trailersContent = EMPTY_TRAILERS.copy( - errorMessage = errorMessage, - ), - ) - } + val errorMessage = "Something went wrong" + discoverRepository.setShowResult(Either.Right(selectedShow)) + seasonsRepository.setSeasonsResult(Either.Right(seasons)) + similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) + trailerRepository.setTrailerResult(Either.Left(ServerError(errorMessage))) + + presenter.state shouldBe EMPTY_DETAIL_STATE + presenter.state shouldBe showDetailsLoaded + presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = seasonsShowDetailsLoaded, + ) + presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = seasonsShowDetailsLoaded, + trailersContent = EMPTY_TRAILERS.copy( + errorMessage = errorMessage, + ), + ) + presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = seasonsShowDetailsLoaded, + similarShowsContent = similarShowLoaded, + trailersContent = EMPTY_TRAILERS.copy( + errorMessage = errorMessage, + ), + ) } @Test fun error_loading_seasons_emits_expected_result() = runTest { - presenter.state.test { - val errorMessage = "Something went wrong" - discoverRepository.setShowResult(Either.Right(selectedShow)) - similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) - trailerRepository.setTrailerResult(Either.Right(trailers)) - seasonsRepository.setSeasonWithEpisodes(Either.Left(ServerError(errorMessage))) - - awaitItem() shouldBe EMPTY_DETAIL_STATE - /* awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = EMPTY_SEASONS.copy( - errorMessage = errorMessage, - ), - ) - awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = EMPTY_SEASONS.copy( - errorMessage = errorMessage, - ), - trailersContent = trailerShowDetailsLoaded, - ) - awaitItem() shouldBe showDetailsLoaded.copy( - seasonsContent = EMPTY_SEASONS.copy( - errorMessage = errorMessage, - ), - trailersContent = trailerShowDetailsLoaded, - similarShowsContent = similarShowLoaded, - )*/ - } + val errorMessage = "Something went wrong" + discoverRepository.setShowResult(Either.Right(selectedShow)) + similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) + trailerRepository.setTrailerResult(Either.Right(trailers)) + seasonsRepository.setSeasonWithEpisodes(Either.Left(ServerError(errorMessage))) + + presenter.state shouldBe EMPTY_DETAIL_STATE + /* presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = EMPTY_SEASONS.copy( + errorMessage = errorMessage, + ), + ) + presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = EMPTY_SEASONS.copy( + errorMessage = errorMessage, + ), + trailersContent = trailerShowDetailsLoaded, + ) + presenter.state shouldBe showDetailsLoaded.copy( + seasonsContent = EMPTY_SEASONS.copy( + errorMessage = errorMessage, + ), + trailersContent = trailerShowDetailsLoaded, + similarShowsContent = similarShowLoaded, + )*/ } @Test fun error_state_emits_expected_result() = runTest { - presenter.state.test { - val errorMessage = "Something went wrong" - discoverRepository.setShowResult(Either.Left(ServerError(errorMessage))) - similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) - trailerRepository.setTrailerResult(Either.Right(trailers)) - seasonsRepository.setSeasonsResult(Either.Right(seasons)) - - awaitItem() shouldBe EMPTY_DETAIL_STATE - awaitItem() shouldBe EMPTY_DETAIL_STATE.copy( - errorMessage = errorMessage, - ) - } + val errorMessage = "Something went wrong" + discoverRepository.setShowResult(Either.Left(ServerError(errorMessage))) + similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) + trailerRepository.setTrailerResult(Either.Right(trailers)) + seasonsRepository.setSeasonsResult(Either.Right(seasons)) + + presenter.state shouldBe EMPTY_DETAIL_STATE + presenter.state shouldBe EMPTY_DETAIL_STATE.copy( + errorMessage = errorMessage, + ) } } diff --git a/presentation/trailers/build.gradle.kts b/presentation/trailers/build.gradle.kts index d801ffdb0..471ee50e1 100644 --- a/presentation/trailers/build.gradle.kts +++ b/presentation/trailers/build.gradle.kts @@ -11,6 +11,7 @@ kotlin { implementation(projects.data.trailers.api) api(libs.decompose.decompose) + api(libs.essenty.lifecycle) api(libs.kotlinx.collections) implementation(libs.kotlinInject.runtime) diff --git a/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersPresenter.kt b/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersPresenter.kt index bac1014fe..2315aecd3 100644 --- a/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersPresenter.kt +++ b/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersPresenter.kt @@ -1,13 +1,12 @@ package com.thomaskioko.tvmaniac.presentation.trailers import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.value.Value import com.thomaskioko.tvmaniac.data.trailers.implementation.TrailerRepository -import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.decompose.asValue +import com.thomaskioko.tvmaniac.util.decompose.coroutineScope import com.thomaskioko.tvmaniac.util.model.Either -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -20,15 +19,15 @@ typealias TrailersPresenterFactory = ( ) -> TrailersPresenter class TrailersPresenter @Inject constructor( - dispatchersProvider: AppCoroutineDispatchers, @Assisted componentContext: ComponentContext, @Assisted private val traktShowId: Long, private val repository: TrailerRepository, ) : ComponentContext by componentContext { - private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchersProvider.main) + private val coroutineScope = coroutineScope() private val _state = MutableStateFlow(LoadingTrailers) - val state = _state.asStateFlow() + val state: Value = _state + .asValue(initialValue = _state.value, lifecycle = lifecycle) init { coroutineScope.launch { diff --git a/presentation/trailers/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersPresenterTest.kt b/presentation/trailers/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersPresenterTest.kt index 16cb43e4f..23de96940 100644 --- a/presentation/trailers/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersPresenterTest.kt +++ b/presentation/trailers/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersPresenterTest.kt @@ -1,6 +1,5 @@ package com.thomaskioko.tvmaniac.presentation.trailers -import app.cash.turbine.test import com.thomaskioko.tvmaniac.presentation.trailers.model.Trailer import com.thomaskioko.tvmaniac.trailers.testing.FakeTrailerRepository import com.thomaskioko.tvmaniac.trailers.testing.trailers @@ -43,62 +42,58 @@ internal class TrailersPresenterTest { @Test fun `given result is success correct state is emitted`() = runTest { - presenter.state.test { - repository.setTrailerList(trailers) + repository.setTrailerList(trailers) - awaitItem() shouldBe LoadingTrailers - awaitItem() shouldBe TrailersContent( - selectedVideoKey = "Fd43V", - trailersList = persistentListOf( - Trailer( - showId = 84958, - key = "Fd43V", - name = "Some title", - youtubeThumbnailUrl = "https://i.ytimg.com/vi/Fd43V/hqdefault.jpg", - ), + presenter.state shouldBe LoadingTrailers + presenter.state shouldBe TrailersContent( + selectedVideoKey = "Fd43V", + trailersList = persistentListOf( + Trailer( + showId = 84958, + key = "Fd43V", + name = "Some title", + youtubeThumbnailUrl = "https://i.ytimg.com/vi/Fd43V/hqdefault.jpg", ), - ) - } + ), + ) } @Test fun `given reload is clicked then correct state is emitted`() = runTest { - presenter.state.test { - repository.setTrailerList(trailers) + repository.setTrailerList(trailers) - repository.setTrailerResult(Either.Left(ServerError("Something went wrong."))) + repository.setTrailerResult(Either.Left(ServerError("Something went wrong."))) - awaitItem() shouldBe LoadingTrailers - awaitItem() shouldBe TrailersContent( - selectedVideoKey = "Fd43V", - trailersList = persistentListOf( - Trailer( - showId = 84958, - key = "Fd43V", - name = "Some title", - youtubeThumbnailUrl = "https://i.ytimg.com/vi/Fd43V/hqdefault.jpg", - ), + presenter.state shouldBe LoadingTrailers + presenter.state shouldBe TrailersContent( + selectedVideoKey = "Fd43V", + trailersList = persistentListOf( + Trailer( + showId = 84958, + key = "Fd43V", + name = "Some title", + youtubeThumbnailUrl = "https://i.ytimg.com/vi/Fd43V/hqdefault.jpg", ), - ) + ), + ) - awaitItem() shouldBe TrailerError("Something went wrong.") + presenter.state shouldBe TrailerError("Something went wrong.") - presenter.dispatch(ReloadTrailers) + presenter.dispatch(ReloadTrailers) - repository.setTrailerResult(Either.Right(trailers)) + repository.setTrailerResult(Either.Right(trailers)) - awaitItem() shouldBe LoadingTrailers - awaitItem() shouldBe TrailersContent( - selectedVideoKey = "Fd43V", - trailersList = persistentListOf( - Trailer( - showId = 84958, - key = "Fd43V", - name = "Some title", - youtubeThumbnailUrl = "https://i.ytimg.com/vi/Fd43V/hqdefault.jpg", - ), + presenter.state shouldBe LoadingTrailers + presenter.state shouldBe TrailersContent( + selectedVideoKey = "Fd43V", + trailersList = persistentListOf( + Trailer( + showId = 84958, + key = "Fd43V", + name = "Some title", + youtubeThumbnailUrl = "https://i.ytimg.com/vi/Fd43V/hqdefault.jpg", ), - ) - } + ), + ) } } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index dd9041476..d573e7540 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -2,12 +2,48 @@ import com.thomaskioko.tvmaniac.plugins.addKspDependencyForAllTargets plugins { id("plugin.tvmaniac.kotlin.android") - id("plugin.tvmaniac.multiplatform") + id("org.jetbrains.kotlin.multiplatform") id("com.chromaticnoise.multiplatform-swiftpackage") version "2.0.3" + id("co.touchlab.skie") version "0.5.6" alias(libs.plugins.ksp) } +version = libs.versions.shared.module.version.get() + kotlin { + + androidTarget() + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { + it.binaries.framework { + baseName = "TvManiac" + isStatic = true + linkerOpts.add("-lsqlite3") + freeCompilerArgs += "-Xadd-light-debug=enable" + + export(projects.navigation) + export(projects.core.datastore.api) + export(projects.presentation.discover) + export(projects.presentation.library) + export(projects.presentation.moreShows) + export(projects.presentation.profile) + export(projects.presentation.search) + export(projects.presentation.seasondetails) + export(projects.presentation.settings) + export(projects.presentation.showDetails) + export(projects.presentation.trailers) + + export(libs.decompose.decompose) + export(libs.essenty.lifecycle) + } + } + + applyDefaultHierarchyTemplate() + sourceSets { commonMain { dependencies { @@ -62,6 +98,9 @@ kotlin { api(projects.data.similar.implementation) api(projects.data.trailers.api) api(projects.data.trailers.implementation) + + api(libs.decompose.decompose) + api(libs.essenty.lifecycle) } } } diff --git a/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/ApplicationComponent.kt b/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/ApplicationComponent.kt index 74a4f9e37..3775b4e51 100644 --- a/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/ApplicationComponent.kt +++ b/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/ApplicationComponent.kt @@ -1,49 +1,10 @@ package com.thomaskioko.tvmaniac.shared -import com.thomaskioko.trakt.service.implementation.inject.TraktComponent -import com.thomaskioko.tvmaniac.data.category.implementation.CategoryComponent -import com.thomaskioko.tvmaniac.data.trailers.implementation.TrailerComponent -import com.thomaskioko.tvmaniac.datastore.implementation.DataStoreComponent -import com.thomaskioko.tvmaniac.db.DatabaseComponent -import com.thomaskioko.tvmaniac.episodeimages.implementation.EpisodeImageComponent -import com.thomaskioko.tvmaniac.episodes.implementation.EpisodeComponent -import com.thomaskioko.tvmaniac.profile.implementation.ProfileComponent -import com.thomaskioko.tvmaniac.profilestats.implementation.StatsComponent -import com.thomaskioko.tvmaniac.resourcemanager.implementation.RequestManagerComponent -import com.thomaskioko.tvmaniac.seasondetails.implementation.SeasonDetailsComponent -import com.thomaskioko.tvmaniac.seasons.implementation.SeasonsComponent -import com.thomaskioko.tvmaniac.showimages.implementation.ShowImagesComponent -import com.thomaskioko.tvmaniac.shows.implementation.DiscoverComponent -import com.thomaskioko.tvmaniac.similar.implementation.SimilarShowsComponent -import com.thomaskioko.tvmaniac.tmdb.implementation.TmdbComponent -import com.thomaskioko.tvmaniac.traktauth.implementation.TraktAuthenticationComponent -import com.thomaskioko.tvmaniac.util.inject.UtilPlatformComponent import com.thomaskioko.tvmaniac.util.scope.ApplicationScope -import com.thomaskioko.tvmaniac.watchlist.implementation.LibraryComponent import me.tatarka.inject.annotations.Component -@ApplicationScope @Component -abstract class ApplicationComponent : - CategoryComponent, - DatabaseComponent, - DataStoreComponent, - EpisodeComponent, - EpisodeImageComponent, - ProfileComponent, - RequestManagerComponent, - SeasonsComponent, - SeasonDetailsComponent, - DiscoverComponent, - ShowImagesComponent, - SimilarShowsComponent, - StatsComponent, - TmdbComponent, - TraktComponent, - TraktAuthenticationComponent, - TrailerComponent, - UtilPlatformComponent, - LibraryComponent { - +@ApplicationScope +abstract class ApplicationComponent : SharedComponent() { companion object } diff --git a/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/IosViewPresenterComponent.kt b/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/IosViewPresenterComponent.kt new file mode 100644 index 000000000..a9a99ea3c --- /dev/null +++ b/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/IosViewPresenterComponent.kt @@ -0,0 +1,19 @@ +package com.thomaskioko.tvmaniac.shared + +import com.arkivanov.decompose.ComponentContext +import com.thomaskioko.tvmaniac.navigation.RootNavigationPresenter +import com.thomaskioko.tvmaniac.traktauth.implementation.TraktAuthManagerComponent +import com.thomaskioko.tvmaniac.util.scope.ActivityScope +import me.tatarka.inject.annotations.Component +import me.tatarka.inject.annotations.Provides + +@Component +@ActivityScope +abstract class IosViewPresenterComponent( + @get:Provides val componentContext: ComponentContext, + @Component val applicationComponent: ApplicationComponent, +) : TraktAuthManagerComponent { + abstract val presenter: RootNavigationPresenter + + companion object +} diff --git a/tooling/plugins/src/main/kotlin/com/thomaskioko/tvmaniac/plugins/KotlinMultiplatformConventionPlugin.kt b/tooling/plugins/src/main/kotlin/com/thomaskioko/tvmaniac/plugins/KotlinMultiplatformConventionPlugin.kt index f704880a9..6dc49823a 100644 --- a/tooling/plugins/src/main/kotlin/com/thomaskioko/tvmaniac/plugins/KotlinMultiplatformConventionPlugin.kt +++ b/tooling/plugins/src/main/kotlin/com/thomaskioko/tvmaniac/plugins/KotlinMultiplatformConventionPlugin.kt @@ -19,7 +19,7 @@ class KotlinMultiplatformConventionPlugin : Plugin { apply("org.jetbrains.kotlin.multiplatform") } - version = libs.findVersion("shared-module") + version = libs.findVersion("shared-module-version") extensions.configure { applyDefaultHierarchyTemplate() @@ -34,15 +34,7 @@ class KotlinMultiplatformConventionPlugin : Plugin { iosX64(), iosArm64(), iosSimulatorArm64(), - ).forEach { target -> - target.binaries.framework { - baseName = path.substring(1).replace(':', '-') - isStatic = true - - linkerOpts.add("-lsqlite3") - freeCompilerArgs += "-Xadd-light-debug=enable" - } - } + ) targets.withType().configureEach { compilations.configureEach {