diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96fccbcfc..6c4c8a177 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,6 +102,9 @@ jobs: - name: unitTest run: ./gradlew testDemoDebug + - name: Jvm Test + run: ./gradlew jvmTest + - uses: actions/upload-artifact@v3 with: name: unit-test-report diff --git a/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Buttons.kt b/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Buttons.kt index f37d9c332..d881c10da 100644 --- a/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Buttons.kt +++ b/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Buttons.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme @@ -93,7 +92,7 @@ fun TvManiacOutlinedButton( borderColor: Color, modifier: Modifier = Modifier, enabled: Boolean = true, - shape: Shape = RoundedCornerShape(4.dp), + shape: Shape = MaterialTheme.shapes.small, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit, ) { diff --git a/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Card.kt b/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Card.kt index 2dd7640c4..1cd86ba95 100644 --- a/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Card.kt +++ b/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Card.kt @@ -14,7 +14,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp @@ -26,6 +26,7 @@ fun TvPosterCard( posterImageUrl: String?, title: String, modifier: Modifier = Modifier, + shape: Shape = MaterialTheme.shapes.small, imageWidth: Dp = 120.dp, onClick: () -> Unit = {}, ) { @@ -33,7 +34,7 @@ fun TvPosterCard( modifier = modifier .width(imageWidth) .clickable { onClick() }, - shape = RectangleShape, + shape = shape, elevation = CardDefaults.cardElevation( defaultElevation = 4.dp, ), diff --git a/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Dialogs.kt b/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Dialogs.kt index c46fdf376..0564f8365 100644 --- a/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Dialogs.kt +++ b/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Dialogs.kt @@ -9,7 +9,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp @@ -24,7 +23,7 @@ fun BasicDialog( enableConfirmButton: Boolean = true, enableDismissButton: Boolean = true, dismissButtonText: String? = null, - shape: Shape = RectangleShape, + shape: Shape = MaterialTheme.shapes.small, onDismissDialog: () -> Unit = {}, confirmButtonClicked: () -> Unit = {}, dismissButtonClicked: () -> Unit = {}, diff --git a/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Shape.kt b/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Shape.kt new file mode 100644 index 000000000..6e386334c --- /dev/null +++ b/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Shape.kt @@ -0,0 +1,11 @@ +package com.thomaskioko.tvmaniac.compose.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +val tvManiacShapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(8.dp), + large = RoundedCornerShape(16.dp), +) diff --git a/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Theme.kt b/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Theme.kt index a949932c5..398d8ae47 100644 --- a/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Theme.kt +++ b/android-core/designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Theme.kt @@ -54,6 +54,7 @@ fun TvManiacTheme( MaterialTheme( colorScheme = colorScheme, typography = tvManiacTypography, + shapes = tvManiacShapes, content = content, ) } diff --git a/android-core/resources/src/main/res/values/strings.xml b/android-core/resources/src/main/res/values/strings.xml index 3e72dc04e..6e8fd09b7 100644 --- a/android-core/resources/src/main/res/values/strings.xml +++ b/android-core/resources/src/main/res/values/strings.xml @@ -40,6 +40,7 @@ Settings All Seasons + All Episodes Watch Next Back Online! No Internet Connection! diff --git a/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverPreviewParameterProvider.kt b/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverPreviewParameterProvider.kt index b0d86f32d..19b4f1c14 100644 --- a/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverPreviewParameterProvider.kt +++ b/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverPreviewParameterProvider.kt @@ -3,6 +3,7 @@ package com.thomaskioko.tvmaniac.discover import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.thomaskioko.tvmaniac.presentation.discover.DataLoaded import com.thomaskioko.tvmaniac.presentation.discover.DiscoverState +import com.thomaskioko.tvmaniac.presentation.discover.ErrorState import com.thomaskioko.tvmaniac.presentation.discover.model.TvShow import kotlinx.collections.immutable.toImmutableList @@ -39,7 +40,7 @@ class DiscoverPreviewParameterProvider : PreviewParameterProvider get() { return sequenceOf( discoverContentSuccess, - DataLoaded(errorMessage = "Opps! Something went wrong"), + ErrorState(errorMessage = "Opps! Something went wrong"), ) } } diff --git a/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt b/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt index 464ab936b..02f532554 100644 --- a/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt +++ b/android-features/discover/src/main/java/com/thomaskioko/tvmaniac/discover/DiscoverScreen.kt @@ -57,7 +57,6 @@ import androidx.compose.ui.util.lerp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.thomaskioko.tvmaniac.category.api.model.Category import com.thomaskioko.tvmaniac.compose.components.BoxTextItems -import com.thomaskioko.tvmaniac.compose.components.EmptyUi import com.thomaskioko.tvmaniac.compose.components.ErrorUi import com.thomaskioko.tvmaniac.compose.components.LoadingIndicator import com.thomaskioko.tvmaniac.compose.components.ThemePreviews @@ -72,6 +71,7 @@ import com.thomaskioko.tvmaniac.compose.util.rememberDominantColorState import com.thomaskioko.tvmaniac.navigation.extensions.viewModel import com.thomaskioko.tvmaniac.presentation.discover.DataLoaded import com.thomaskioko.tvmaniac.presentation.discover.DiscoverState +import com.thomaskioko.tvmaniac.presentation.discover.ErrorState import com.thomaskioko.tvmaniac.presentation.discover.Loading import com.thomaskioko.tvmaniac.presentation.discover.RetryLoading import com.thomaskioko.tvmaniac.presentation.discover.SnackBarDismissed @@ -140,49 +140,33 @@ private fun DiscoverScreen( onMoreClicked: (showType: Long) -> Unit, ) { when (state) { - Loading -> - LoadingIndicator( - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center), - ) - - is DataLoaded -> - when { - state.isContentEmpty && state.errorMessage != null -> { - ErrorUi( - errorMessage = state.errorMessage, - onRetry = onRetry, - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center), - ) - } - - state.isContentEmpty -> { - EmptyUi( - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center), - ) - } - - else -> { - DiscoverScrollContent( - modifier = modifier, - pagerState = pagerState, - snackBarHostState = snackBarHostState, - onShowClicked = onShowClicked, - onMoreClicked = onMoreClicked, - onSnackBarErrorDismissed = onErrorDismissed, - trendingShows = state.trendingShows, - popularShows = state.popularShows, - anticipatedShows = state.anticipatedShows, - recommendedShows = state.recommendedShows, - errorMessage = state.errorMessage, - ) - } - } + Loading -> LoadingIndicator( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center), + ) + + is DataLoaded -> DiscoverScrollContent( + modifier = modifier, + pagerState = pagerState, + snackBarHostState = snackBarHostState, + onShowClicked = onShowClicked, + onMoreClicked = onMoreClicked, + onSnackBarErrorDismissed = onErrorDismissed, + trendingShows = state.trendingShows, + popularShows = state.popularShows, + anticipatedShows = state.anticipatedShows, + recommendedShows = state.recommendedShows, + errorMessage = state.errorMessage, + ) + + is ErrorState -> ErrorUi( + errorMessage = state.errorMessage, + onRetry = onRetry, + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center), + ) } } @@ -433,7 +417,7 @@ fun HorizontalPagerItem( } } -@OptIn(ExperimentalSnapperApi::class) +@OptIn(ExperimentalSnapperApi::class, ExperimentalFoundationApi::class) @Composable private fun RowContent( category: Category, @@ -465,6 +449,8 @@ private fun RowContent( posterImageUrl = tvShow.posterImageUrl, title = tvShow.title, onClick = { onItemClicked(tvShow.traktId) }, + modifier = Modifier + .animateItemPlacement(), ) } } diff --git a/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonDetailsScreen.kt b/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonDetailsScreen.kt index 6d78b3315..92757d8b2 100644 --- a/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonDetailsScreen.kt +++ b/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonDetailsScreen.kt @@ -7,10 +7,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api @@ -41,10 +44,14 @@ import com.thomaskioko.tvmaniac.presentation.seasondetails.Loading import com.thomaskioko.tvmaniac.presentation.seasondetails.LoadingError import com.thomaskioko.tvmaniac.presentation.seasondetails.SeasonDetailsLoaded import com.thomaskioko.tvmaniac.presentation.seasondetails.SeasonDetailsState +import com.thomaskioko.tvmaniac.presentation.seasondetails.model.Episode import com.thomaskioko.tvmaniac.presentation.seasondetails.model.SeasonDetails import com.thomaskioko.tvmaniac.resources.R import com.thomaskioko.tvmaniac.seasondetails.components.CollapsableContent -import com.thomaskioko.tvmaniac.seasondetails.components.WatchNextContent +import com.thomaskioko.tvmaniac.seasondetails.components.EpisodeItem +import dev.chrisbanes.snapper.ExperimentalSnapperApi +import dev.chrisbanes.snapper.rememberSnapperFlingBehavior +import kotlinx.collections.immutable.ImmutableList import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject @@ -106,8 +113,7 @@ internal fun SeasonDetailScreen( navigateUp = onBackClicked, ) }, - modifier = modifier - .statusBarsPadding(), + modifier = modifier.statusBarsPadding(), content = { contentPadding -> when (state) { Loading -> LoadingIndicator( @@ -116,14 +122,13 @@ internal fun SeasonDetailScreen( .wrapContentSize(Alignment.Center), ) - is LoadingError -> - ErrorUi( - errorMessage = state.message, - onRetry = {}, - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center), - ) + is LoadingError -> ErrorUi( + errorMessage = state.message, + onRetry = {}, + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center), + ) is SeasonDetailsLoaded -> { SeasonContent( @@ -155,7 +160,7 @@ private fun TopBar( @Composable private fun SeasonContent( - seasonsEpList: List?, + seasonsEpList: ImmutableList?, initialSeasonName: String?, listState: LazyListState, contentPadding: PaddingValues, @@ -179,7 +184,9 @@ private fun SeasonContent( LazyColumn( state = listState, contentPadding = contentPadding.copy(copyTop = false), - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxSize(), ) { item { Spacer(modifier = Modifier.height(64.dp)) } @@ -187,7 +194,11 @@ private fun SeasonContent( item { Spacer(modifier = Modifier.height(16.dp)) } - item { AllSeasonsTitle() } + item { + LabelTitle( + label = stringResource(id = R.string.title_all_episodes), + ) + } itemsIndexed(seasonsEpList) { index, season -> CollapsableContent( @@ -204,20 +215,57 @@ private fun SeasonContent( } } +@OptIn(ExperimentalSnapperApi::class) +@Composable +fun WatchNextContent( + episodeList: ImmutableList?, + modifier: Modifier = Modifier, + onEpisodeClicked: () -> Unit = {}, +) { + episodeList?.let { + LabelTitle( + modifier = modifier + .padding(top = 16.dp, bottom = 8.dp), + label = stringResource(id = R.string.title_watch_next), + ) + + val lazyListState = rememberLazyListState() + + LazyRow( + state = lazyListState, + flingBehavior = rememberSnapperFlingBehavior(lazyListState), + ) { + itemsIndexed(episodeList) { index, episode -> + val value = if (index == 0) 0 else 8 + Spacer(modifier = Modifier.width(value.dp)) + + EpisodeItem( + modifier = modifier.size(width = 320.dp, height = 90.dp), + imageUrl = episode.imageUrl, + title = episode.seasonEpisodeNumber, + episodeOverview = episode.overview, + onEpisodeClicked = onEpisodeClicked, + ) + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } + } +} + @Composable -private fun AllSeasonsTitle( +private fun LabelTitle( + label: String, modifier: Modifier = Modifier, ) { Box( - modifier = modifier - .fillMaxWidth() - .padding(2.dp), + modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = stringResource(id = R.string.title_all_seasons), + text = label, style = MaterialTheme.typography.labelMedium.copy(MaterialTheme.colorScheme.secondary), ) } @@ -226,8 +274,7 @@ private fun AllSeasonsTitle( @ThemePreviews @Composable private fun SeasonDetailScreenPreview( - @PreviewParameter(SeasonPreviewParameterProvider::class) - state: SeasonDetailsState, + @PreviewParameter(SeasonPreviewParameterProvider::class) state: SeasonDetailsState, ) { TvManiacTheme { Surface { diff --git a/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonPreviewParameterProvider.kt b/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonPreviewParameterProvider.kt index b13afa457..5f2080937 100644 --- a/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonPreviewParameterProvider.kt +++ b/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/SeasonPreviewParameterProvider.kt @@ -6,6 +6,8 @@ import com.thomaskioko.tvmaniac.presentation.seasondetails.SeasonDetailsLoaded import com.thomaskioko.tvmaniac.presentation.seasondetails.SeasonDetailsState import com.thomaskioko.tvmaniac.presentation.seasondetails.model.Episode import com.thomaskioko.tvmaniac.presentation.seasondetails.model.SeasonDetails +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList val episode = Episode( id = 2534997, @@ -27,7 +29,7 @@ val seasonDetails = SeasonDetails( watchProgress = 0.4f, episodes = List(8) { episode - }, + }.toPersistentList(), ) class SeasonPreviewParameterProvider : PreviewParameterProvider { @@ -36,7 +38,7 @@ class SeasonPreviewParameterProvider : PreviewParameterProvider Unit = {}, ) { val transitionState = remember { @@ -112,11 +111,10 @@ private fun SeasonTitleHeader( ) Card( - shape = RectangleShape, + shape = shape, modifier = Modifier .fillMaxWidth() .height(64.dp) - .padding(horizontal = 16.dp) .clickable { onSeasonHeaderClicked() }, ) { ConstraintLayout( @@ -209,108 +207,6 @@ private fun SeasonTitleHeader( } } -@Composable -fun EpisodeItem( - episode: Episode, - modifier: Modifier = Modifier, - onEpisodeClicked: (Long) -> Unit = {}, -) { - Card( - shape = RectangleShape, - modifier = modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 84.dp) - .padding(horizontal = 16.dp) - .clickable { onEpisodeClicked(episode.id) }, - ) { - ConstraintLayout( - modifier = Modifier.fillMaxWidth(), - ) { - val (episodeTitle, image, overview, watchedStatusIcon) = createRefs() - - AsyncImageComposable( - model = episode.imageUrl, - contentDescription = stringResource( - R.string.cd_show_poster, - episode.episodeNumberTitle, - ), - contentScale = ContentScale.Crop, - modifier = Modifier - .width(94.dp) - .constrainAs(image) { - start.linkTo(parent.start) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - - height = Dimension.fillToConstraints - }, - ) - - Text( - text = episode.episodeNumberTitle, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier - .constrainAs(episodeTitle) { - start.linkTo(image.end, 8.dp) - end.linkTo(watchedStatusIcon.start) - top.linkTo(parent.top, 8.dp) - - width = Dimension.fillToConstraints - }, - ) - - Text( - text = episode.overview, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier - .constrainAs(overview) { - start.linkTo(image.end, 8.dp) - top.linkTo(episodeTitle.bottom, 5.dp) - end.linkTo(watchedStatusIcon.start, 8.dp) - bottom.linkTo(parent.bottom, 8.dp) - - width = Dimension.fillToConstraints - }, - ) - - IconButton( - onClick = {}, - modifier = Modifier - .constrainAs(watchedStatusIcon) { - centerVerticallyTo(parent) - end.linkTo(parent.end, 8.dp) - }, - ) { - Icon( - imageVector = Icons.Filled.CheckCircle, - contentDescription = stringResource(R.string.cd_navigate_back), - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier - .size(32.dp), - ) - } - } - } -} - -@ThemePreviews -@Composable -fun EpisodeItemPreview() { - TvManiacTheme { - Surface { - EpisodeItem( - episode = episode, - ) - } - } -} - @ThemePreviews @Composable fun SeasonTitleHeaderPreview() { diff --git a/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/components/WatchNextList.kt b/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/components/EpisodeItem.kt similarity index 53% rename from android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/components/WatchNextList.kt rename to android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/components/EpisodeItem.kt index 57773e9b1..047a9bb7d 100644 --- a/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/components/WatchNextList.kt +++ b/android-features/season-details/src/main/java/com/thomaskioko/tvmaniac/seasondetails/components/EpisodeItem.kt @@ -1,16 +1,9 @@ package com.thomaskioko.tvmaniac.seasondetails.components import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.Card @@ -20,11 +13,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout @@ -32,67 +25,22 @@ import androidx.constraintlayout.compose.Dimension import com.thomaskioko.tvmaniac.compose.components.AsyncImageComposable import com.thomaskioko.tvmaniac.compose.components.ThemePreviews import com.thomaskioko.tvmaniac.compose.theme.TvManiacTheme -import com.thomaskioko.tvmaniac.presentation.seasondetails.model.Episode import com.thomaskioko.tvmaniac.resources.R import com.thomaskioko.tvmaniac.seasondetails.episode -import dev.chrisbanes.snapper.ExperimentalSnapperApi -import dev.chrisbanes.snapper.rememberSnapperFlingBehavior -@OptIn(ExperimentalSnapperApi::class) @Composable -fun WatchNextContent( - episodeList: List?, +fun EpisodeItem( + imageUrl: String?, + title: String, + episodeOverview: String, modifier: Modifier = Modifier, -) { - episodeList?.let { - Box( - modifier = modifier - .fillMaxWidth() - .padding(top = 16.dp), - contentAlignment = Alignment.Center, - ) { - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = stringResource(id = R.string.title_watch_next), - style = MaterialTheme.typography.labelMedium.copy(MaterialTheme.colorScheme.secondary), - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - val lazyListState = rememberLazyListState() - - LazyRow( - state = lazyListState, - flingBehavior = rememberSnapperFlingBehavior(lazyListState), - ) { - itemsIndexed(episodeList) { index, episode -> - val value = if (index == 0) 32 else 8 - Spacer(modifier = Modifier.width(value.dp)) - - WatchNextItem( - episode = episode, - onEpisodeClicked = {}, - ) - } - - item { Spacer(modifier = Modifier.height(16.dp)) } - } - } -} - -@Composable -fun WatchNextItem( - episode: Episode, - modifier: Modifier = Modifier, - onEpisodeClicked: (Long) -> Unit = {}, + shape: Shape = MaterialTheme.shapes.small, + onEpisodeClicked: () -> Unit = {}, ) { Card( - shape = RectangleShape, + shape = shape, modifier = modifier - .size(width = 260.dp, height = 90.dp) - .clickable { onEpisodeClicked(episode.id) }, + .clickable { onEpisodeClicked() }, ) { ConstraintLayout( modifier = Modifier.fillMaxWidth(), @@ -100,54 +48,51 @@ fun WatchNextItem( val (episodeTitle, image, overview, watchedStatusIcon) = createRefs() AsyncImageComposable( - model = episode.imageUrl, + model = imageUrl, contentDescription = stringResource( R.string.cd_show_poster, - episode.episodeNumberTitle, + title, ), contentScale = ContentScale.Crop, modifier = Modifier - .width(84.dp) + .width(94.dp) .constrainAs(image) { start.linkTo(parent.start) - bottom.linkTo(parent.bottom) top.linkTo(parent.top) + bottom.linkTo(parent.bottom) height = Dimension.fillToConstraints }, ) Text( - text = episode.seasonEpisodeNumber, + text = title, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, modifier = Modifier .constrainAs(episodeTitle) { - linkTo( - start = image.end, - end = watchedStatusIcon.start, - startMargin = 8.dp, - bias = 0f, - ) - top.linkTo(parent.top, 16.dp) + start.linkTo(image.end, 8.dp) + end.linkTo(watchedStatusIcon.start) + top.linkTo(parent.top, 8.dp) - width = Dimension.preferredWrapContent + width = Dimension.fillToConstraints }, ) Text( - text = episode.episodeTitle, - maxLines = 1, + text = episodeOverview, + maxLines = 3, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.bodyMedium, modifier = Modifier - .padding(bottom = 8.dp) .constrainAs(overview) { start.linkTo(image.end, 8.dp) top.linkTo(episodeTitle.bottom, 5.dp) end.linkTo(watchedStatusIcon.start, 8.dp) - bottom.linkTo(parent.bottom) + bottom.linkTo(parent.bottom, 8.dp) width = Dimension.fillToConstraints }, @@ -163,7 +108,7 @@ fun WatchNextItem( ) { Icon( imageVector = Icons.Filled.CheckCircle, - contentDescription = null, + contentDescription = stringResource(R.string.cd_navigate_back), tint = MaterialTheme.colorScheme.onSurface, modifier = Modifier .size(32.dp), @@ -178,8 +123,10 @@ fun WatchNextItem( fun WatchlistRowItemPreview() { TvManiacTheme { Surface { - WatchNextItem( - episode = episode, + EpisodeItem( + title = episode.episodeNumberTitle, + episodeOverview = episode.overview, + imageUrl = episode.imageUrl, ) } } diff --git a/android-features/show-details/src/main/kotlin/com/thomaskioko/showdetails/ShowDetailScreen.kt b/android-features/show-details/src/main/kotlin/com/thomaskioko/showdetails/ShowDetailScreen.kt index 2b7f07cc2..c9ad89deb 100644 --- a/android-features/show-details/src/main/kotlin/com/thomaskioko/showdetails/ShowDetailScreen.kt +++ b/android-features/show-details/src/main/kotlin/com/thomaskioko/showdetails/ShowDetailScreen.kt @@ -1,6 +1,7 @@ package com.thomaskioko.showdetails import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -670,7 +671,7 @@ private fun TrailersContent( } } -@OptIn(ExperimentalSnapperApi::class) +@OptIn(ExperimentalSnapperApi::class, ExperimentalFoundationApi::class) @Composable fun SimilarShowsContent( isLoading: Boolean, @@ -696,6 +697,8 @@ fun SimilarShowsContent( Spacer(modifier = Modifier.width(value.dp)) TvPosterCard( + modifier = Modifier + .animateItemPlacement(), posterImageUrl = tvShow.posterImageUrl, title = tvShow.title, onClick = { onShowClicked(tvShow.traktId) }, diff --git a/android-features/shows-grid/src/main/kotlin/com/thomaskioko/tvmaniac/showsgrid/GridStateMachine.kt b/android-features/shows-grid/src/main/kotlin/com/thomaskioko/tvmaniac/showsgrid/GridStateMachine.kt index 2a42501ed..52008a7f7 100644 --- a/android-features/shows-grid/src/main/kotlin/com/thomaskioko/tvmaniac/showsgrid/GridStateMachine.kt +++ b/android-features/shows-grid/src/main/kotlin/com/thomaskioko/tvmaniac/showsgrid/GridStateMachine.kt @@ -3,13 +3,13 @@ package com.thomaskioko.tvmaniac.showsgrid import com.freeletics.flowredux.dsl.ChangedState import com.freeletics.flowredux.dsl.FlowReduxStateMachine import com.freeletics.flowredux.dsl.State +import com.thomaskioko.tvmaniac.category.api.model.getCategory import com.thomaskioko.tvmaniac.shows.api.DiscoverRepository +import com.thomaskioko.tvmaniac.util.model.Either import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import me.tatarka.inject.annotations.Inject -import org.mobilenativefoundation.store.store5.StoreReadResponse -@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class) @Inject class GridStateMachine( private val repository: DiscoverRepository, @@ -39,22 +39,18 @@ class GridStateMachine( action: LoadShows, ): ChangedState { var nextState: ChangedState = state.noChange() - repository.observeShowsByCategory(action.category) + repository.observeShowCategory(category = action.category.getCategory()) .collect { result -> nextState = when (result) { - is StoreReadResponse.NoNewData -> state.noChange() - is StoreReadResponse.Loading -> state.override { LoadingContent } - is StoreReadResponse.Data -> state.override { + is Either.Left -> state.override { + LoadingContentError(result.error.errorMessage) + } + + is Either.Right -> state.override { ShowsLoaded( - list = result.requireData().toTvShowList(), + list = result.data.map { it.toTvShow() }, ) } - is StoreReadResponse.Error.Exception -> state.override { - LoadingContentError(result.error.message) - } - is StoreReadResponse.Error.Message -> state.override { - LoadingContentError(result.message) - } } } diff --git a/android-features/shows-grid/src/main/kotlin/com/thomaskioko/tvmaniac/showsgrid/Mapper.kt b/android-features/shows-grid/src/main/kotlin/com/thomaskioko/tvmaniac/showsgrid/Mapper.kt index f16897513..8133b8d22 100644 --- a/android-features/shows-grid/src/main/kotlin/com/thomaskioko/tvmaniac/showsgrid/Mapper.kt +++ b/android-features/shows-grid/src/main/kotlin/com/thomaskioko/tvmaniac/showsgrid/Mapper.kt @@ -3,10 +3,8 @@ package com.thomaskioko.tvmaniac.showsgrid import com.thomaskioko.tvmaniac.core.db.ShowsByCategory import com.thomaskioko.tvmaniac.showsgrid.model.TvShow -fun List.toTvShowList(): List = map { it.toTvShow() } - fun ShowsByCategory.toTvShow(): TvShow = TvShow( - traktId = trakt_id, + traktId = id.id, tmdbId = tmdb_id, title = title, posterImageUrl = poster_url, diff --git a/android-features/watchlist/src/main/kotlin/com/thomaskioko/tvmaniac/watchlist/WatchlistScreen.kt b/android-features/watchlist/src/main/kotlin/com/thomaskioko/tvmaniac/watchlist/WatchlistScreen.kt index 448dcaba9..b4db906cb 100644 --- a/android-features/watchlist/src/main/kotlin/com/thomaskioko/tvmaniac/watchlist/WatchlistScreen.kt +++ b/android-features/watchlist/src/main/kotlin/com/thomaskioko/tvmaniac/watchlist/WatchlistScreen.kt @@ -1,5 +1,6 @@ package com.thomaskioko.tvmaniac.watchlist +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.statusBarsPadding @@ -113,6 +114,7 @@ private fun WatchlistScreen( ) } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun FollowingGridContent( list: List, @@ -128,6 +130,8 @@ private fun FollowingGridContent( ) { show -> TvPosterCard( + modifier = Modifier + .animateItemPlacement(), posterImageUrl = show.posterImageUrl, title = show.title, onClick = { onItemClicked(show.traktId) }, diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 55798b445..f3b931cbc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,6 +23,7 @@ android { packaging { resources { excludes.add("/META-INF/{AL2.0,LGPL2.1}") + excludes.add("/META-INF/versions/9/previous-compilation-data.bin") } } } @@ -44,7 +45,6 @@ dependencies { implementation(projects.androidFeatures.watchlist) implementation(projects.core.database) - implementation(projects.core.networkutil) implementation(projects.core.util) implementation(projects.core.datastore.api) implementation(projects.core.datastore.implementation) diff --git a/app/src/main/kotlin/com/thomaskioko/tvmaniac/inject/ApplicationComponent.kt b/app/src/main/kotlin/com/thomaskioko/tvmaniac/inject/ApplicationComponent.kt index b4dd2eb2f..1c2518ed5 100644 --- a/app/src/main/kotlin/com/thomaskioko/tvmaniac/inject/ApplicationComponent.kt +++ b/app/src/main/kotlin/com/thomaskioko/tvmaniac/inject/ApplicationComponent.kt @@ -3,12 +3,10 @@ package com.thomaskioko.tvmaniac.inject import android.app.Application import android.content.Context import com.thomaskioko.trakt.service.implementation.inject.TraktComponent -import com.thomaskioko.trakt.service.implementation.inject.TraktPlatformComponent import com.thomaskioko.tvmaniac.TvManicApplication -import com.thomaskioko.tvmaniac.core.networkutil.inject.NetworkPlatformComponent import com.thomaskioko.tvmaniac.data.category.implementation.CategoryComponent import com.thomaskioko.tvmaniac.data.trailers.implementation.TrailerComponent -import com.thomaskioko.tvmaniac.datastore.implementation.DataStorePlatformComponent +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 @@ -21,7 +19,7 @@ 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.TmdbPlatformComponent +import com.thomaskioko.tvmaniac.tmdb.implementation.TmdbComponent import com.thomaskioko.tvmaniac.traktauth.implementation.TraktAuthComponent import com.thomaskioko.tvmaniac.traktauth.implementation.TraktAuthenticationComponent import com.thomaskioko.tvmaniac.util.inject.UtilPlatformComponent @@ -37,10 +35,9 @@ import me.tatarka.inject.annotations.Provides abstract class ApplicationComponent( @get:Provides val application: Application, ) : UtilPlatformComponent, - NetworkPlatformComponent, CategoryComponent, DatabaseComponent, - DataStorePlatformComponent, + DataStoreComponent, EpisodeComponent, EpisodeImageComponent, WatchlistComponent, @@ -54,9 +51,8 @@ abstract class ApplicationComponent( SimilarShowsComponent, StatsComponent, TasksComponent, - TmdbPlatformComponent, + TmdbComponent, TraktComponent, - TraktPlatformComponent, TrailerComponent, TraktAuthComponent, TraktAuthenticationComponent { diff --git a/core/database/src/androidMain/kotlin/com/thomaskioko/tvmaniac/db/DatabaseComponent.kt b/core/database/src/androidMain/kotlin/com/thomaskioko/tvmaniac/db/DatabasePlatformComponent.kt similarity index 52% rename from core/database/src/androidMain/kotlin/com/thomaskioko/tvmaniac/db/DatabaseComponent.kt rename to core/database/src/androidMain/kotlin/com/thomaskioko/tvmaniac/db/DatabasePlatformComponent.kt index 0115d508c..f1f79a2c6 100644 --- a/core/database/src/androidMain/kotlin/com/thomaskioko/tvmaniac/db/DatabaseComponent.kt +++ b/core/database/src/androidMain/kotlin/com/thomaskioko/tvmaniac/db/DatabasePlatformComponent.kt @@ -3,13 +3,11 @@ package com.thomaskioko.tvmaniac.db import android.app.Application import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver -import com.thomaskioko.tvmaniac.core.db.Last_requests -import com.thomaskioko.tvmaniac.core.db.Show import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase import com.thomaskioko.tvmaniac.util.scope.ApplicationScope import me.tatarka.inject.annotations.Provides -actual interface DatabaseComponent { +actual interface DatabasePlatformComponent { @ApplicationScope @Provides @@ -20,18 +18,4 @@ actual interface DatabaseComponent { context = application, name = "tvShows.db", ) - - @ApplicationScope - @Provides - fun provideTvManiacDatabase( - sqlDriver: SqlDriver, - ): TvManiacDatabase = TvManiacDatabase( - driver = sqlDriver, - showAdapter = Show.Adapter( - genresAdapter = stringColumnAdapter, - ), - last_requestsAdapter = Last_requests.Adapter( - timestampAdapter = InstantColumnAdapter, - ), - ) } diff --git a/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DatabaseComponent.kt b/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DatabaseComponent.kt deleted file mode 100644 index 10b34033d..000000000 --- a/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DatabaseComponent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.thomaskioko.tvmaniac.db - -expect interface DatabaseComponent diff --git a/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DatabaseIdsExt.kt b/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DatabaseIdsExt.kt new file mode 100644 index 000000000..55561c0be --- /dev/null +++ b/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DatabaseIdsExt.kt @@ -0,0 +1,24 @@ +package com.thomaskioko.tvmaniac.db + +import kotlin.jvm.JvmInline + +@JvmInline +value class CategoryId(val id: Long) + +@JvmInline +value class EpisodeId(val traktId: Long) + +@JvmInline +value class EpisodeImageId(val traktId: Long) + +@JvmInline +value class SeasonId(val id: Long) + +@JvmInline +value class ShowId(val traktId: Long) + +@JvmInline +value class SimilarShowId(val id: Long) + +@JvmInline +value class Id(val id: Long) diff --git a/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DatabasePlatformComponent.kt b/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DatabasePlatformComponent.kt new file mode 100644 index 000000000..1c7284223 --- /dev/null +++ b/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DatabasePlatformComponent.kt @@ -0,0 +1,70 @@ +package com.thomaskioko.tvmaniac.db + +import app.cash.sqldelight.db.SqlDriver +import com.thomaskioko.tvmaniac.core.db.Episode +import com.thomaskioko.tvmaniac.core.db.Episode_image +import com.thomaskioko.tvmaniac.core.db.Last_requests +import com.thomaskioko.tvmaniac.core.db.Season +import com.thomaskioko.tvmaniac.core.db.Show +import com.thomaskioko.tvmaniac.core.db.Show_category +import com.thomaskioko.tvmaniac.core.db.Show_image +import com.thomaskioko.tvmaniac.core.db.Similar_shows +import com.thomaskioko.tvmaniac.core.db.Trailers +import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase +import com.thomaskioko.tvmaniac.core.db.Watchlist +import com.thomaskioko.tvmaniac.util.scope.ApplicationScope +import me.tatarka.inject.annotations.Provides + +expect interface DatabasePlatformComponent + +interface DatabaseComponent : DatabasePlatformComponent { + @ApplicationScope + @Provides + fun provideTvManiacDatabase( + sqlDriver: SqlDriver, + ): TvManiacDatabase = TvManiacDatabase( + driver = sqlDriver, + showAdapter = Show.Adapter( + genresAdapter = stringColumnAdapter, + idAdapter = IdAdapter(), + ), + last_requestsAdapter = Last_requests.Adapter( + timestampAdapter = InstantColumnAdapter, + ), + episode_imageAdapter = Episode_image.Adapter( + idAdapter = IdAdapter(), + tmdb_idAdapter = IdAdapter(), + ), + episodeAdapter = Episode.Adapter( + idAdapter = IdAdapter(), + season_idAdapter = IdAdapter(), + ), + seasonAdapter = Season.Adapter( + idAdapter = IdAdapter(), + show_idAdapter = IdAdapter(), + ), + show_imageAdapter = Show_image.Adapter( + idAdapter = IdAdapter(), + ), + similar_showsAdapter = Similar_shows.Adapter( + idAdapter = IdAdapter(), + similar_show_idAdapter = IdAdapter(), + ), + watchlistAdapter = Watchlist.Adapter( + idAdapter = IdAdapter(), + ), + show_categoryAdapter = Show_category.Adapter( + idAdapter = IdAdapter(), + category_idAdapter = IdAdapter(), + ), + trailersAdapter = Trailers.Adapter( + show_idAdapter = IdAdapter(), + ), + ) + + @ApplicationScope + @Provides + fun provideDbTransactionRunner( + bind: DbTransactionRunner, + ): DatabaseTransactionRunner = bind +} diff --git a/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DbTransactionRunner.kt b/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DbTransactionRunner.kt new file mode 100644 index 000000000..08febe361 --- /dev/null +++ b/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/DbTransactionRunner.kt @@ -0,0 +1,17 @@ +package com.thomaskioko.tvmaniac.db + +import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase +import me.tatarka.inject.annotations.Inject + +interface DatabaseTransactionRunner { + operator fun invoke(block: () -> T): T +} + +@Inject +class DbTransactionRunner(private val db: TvManiacDatabase) : DatabaseTransactionRunner { + override fun invoke(block: () -> T): T { + return db.transactionWithResult { + block() + } + } +} diff --git a/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/IdAdapter.kt b/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/IdAdapter.kt new file mode 100644 index 000000000..957180058 --- /dev/null +++ b/core/database/src/commonMain/kotlin/com.thomaskioko.tvmaniac.db/IdAdapter.kt @@ -0,0 +1,8 @@ +package com.thomaskioko.tvmaniac.db + +import app.cash.sqldelight.ColumnAdapter + +internal class IdAdapter : ColumnAdapter, Long> { + override fun decode(databaseValue: Long): Id = Id(id = databaseValue) + override fun encode(value: Id): Long = value.id +} diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/SeasonEpisodes.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/SeasonEpisodes.sq deleted file mode 100644 index 58284cffd..000000000 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/SeasonEpisodes.sq +++ /dev/null @@ -1,30 +0,0 @@ -CREATE TABLE season_episodes ( -show_id INTEGER DEFAULT NULL PRIMARY KEY, -season_id INTEGER DEFAULT NULL, -season_number INTEGER DEFAULT NULL, -FOREIGN KEY (season_id) REFERENCES seasons(id), -FOREIGN KEY (show_id) REFERENCES show(trakt_id) -); - -insertOrReplace: -INSERT OR REPLACE INTO season_episodes( -show_id, -season_id, -season_number -)VALUES(?,?,?); - -seasonWithEpisodes: -SELECT show.trakt_id, show.tmdb_id ,show.title, seasons.id , seasons.name, seasons.season_number, seasons.episode_count, -episodes.trakt_id, episodes.season_id, episodes.title , episodes.episode_number, episodes.overview, episodes.runtime, -episodes.ratings, episodes.votes, episode_image.image_url -FROM show -INNER JOIN seasons ON seasons.show_trakt_id = show.trakt_id -INNER JOIN episodes ON episodes.season_id = seasons.id -LEFT OUTER JOIN episode_image ON episode_image.trakt_id = episodes.trakt_id -WHERE show.trakt_id = ? AND season_number != 0 ORDER BY seasons.season_number, episode_number ASC; - -delete: -DELETE FROM season_episodes WHERE season_id = ?; - -deleteAll: -DELETE FROM season_episodes; \ No newline at end of file diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/episode_image.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/episode_image.sq index 8df845156..f8b3111fa 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/episode_image.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/episode_image.sq @@ -1,28 +1,43 @@ +import com.thomaskioko.tvmaniac.db.EpisodeId; +import com.thomaskioko.tvmaniac.db.EpisodeImageId; +import com.thomaskioko.tvmaniac.db.Id; + CREATE TABLE episode_image ( -trakt_id INTEGER NOT NULL PRIMARY KEY, -tmdb_id INTEGER NOT NULL, -image_url TEXT DEFAULT NULL, -FOREIGN KEY (trakt_id) REFERENCES episodes(trakt_id) ON DELETE CASCADE + id INTEGER AS Id NOT NULL PRIMARY KEY, + tmdb_id INTEGER AS Id, + image_url TEXT DEFAULT NULL, + FOREIGN KEY(id) REFERENCES episode(id) ON DELETE CASCADE, + UNIQUE(id) ); insertOrReplace: INSERT OR REPLACE INTO episode_image( -trakt_id, -tmdb_id, -image_url + id, + tmdb_id, + image_url ) VALUES(?,?,?); episodeImage: -SELECT show.tmdb_id, seasons.season_number, episodes.trakt_id, episodes.episode_number -FROM episodes -LEFT JOIN seasons ON seasons.id = episodes.season_id -LEFT JOIN show ON show.trakt_id = seasons.show_trakt_id -LEFT OUTER JOIN episode_image ON episode_image.trakt_id = episodes.trakt_id -WHERE episode_image.image_url IS NULL; +SELECT + show.tmdb_id, + season.season_number, + episode.id, + episode.episode_number, + episode_image.image_url +FROM + episode +LEFT JOIN + season ON season.id = episode.season_id +LEFT JOIN + show ON show.id = season.show_id +LEFT OUTER JOIN + episode_image ON episode_image.id = episode.id +WHERE + show.id = :showId AND episode_image.image_url IS NULL; delete: -DELETE FROM episode_image WHERE trakt_id = ?; +DELETE FROM episode_image WHERE id = ?; deleteAll: DELETE FROM episode_image; \ No newline at end of file diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/episodes.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/episodes.sq index 205600cd7..de0ceb564 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/episodes.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/episodes.sq @@ -1,40 +1,48 @@ -CREATE TABLE episodes ( -trakt_id INTEGER NOT NULL PRIMARY KEY, -season_id INTEGER NOT NULL, -tmdb_id INTEGER, -title TEXT NOT NULL, -overview TEXT NOT NULL, -ratings REAL NOT NULL, -runtime INTEGER NOT NULL, -votes INTEGER NOT NULL, -episode_number TEXT NOT NULL, -FOREIGN KEY (season_id) REFERENCES seasons(id) +import com.thomaskioko.tvmaniac.db.EpisodeId; +import com.thomaskioko.tvmaniac.db.Id; +import com.thomaskioko.tvmaniac.db.SeasonId; + +CREATE TABLE episode ( + id INTEGER AS Id NOT NULL PRIMARY KEY, + season_id INTEGER AS Id NOT NULL, + tmdb_id INTEGER, + title TEXT NOT NULL, + overview TEXT NOT NULL, + ratings REAL NOT NULL, + runtime INTEGER NOT NULL, + votes INTEGER NOT NULL, + episode_number TEXT NOT NULL, + FOREIGN KEY(season_id) REFERENCES season(id) ); insertOrReplace: -INSERT OR REPLACE INTO episodes( -trakt_id, -season_id, -tmdb_id, -title, -overview, -ratings, -runtime, -votes, -episode_number +INSERT OR REPLACE INTO episode( + id, + season_id, + tmdb_id, + title, + overview, + ratings, + runtime, + votes, + episode_number ) VALUES(?,?,?,?,?,?,?,?,?); -episodeById: +episodesById: SELECT * -FROM episodes -LEFT OUTER JOIN episode_image ON episode_image.trakt_id = episodes.trakt_id -WHERE episodes.trakt_id = ? -ORDER BY episode_number ASC ; +FROM + episode +LEFT OUTER JOIN + episode_image ON episode_image.id = episode.id +WHERE + episode.id = ? +ORDER BY + episode_number ASC; delete: -DELETE FROM episodes WHERE trakt_id = ?; +DELETE FROM episode WHERE id = ?; deleteAll: -DELETE FROM episodes; +DELETE FROM episode; diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/season.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/season.sq index 3c136c079..b1cc881b0 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/season.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/season.sq @@ -1,32 +1,76 @@ -CREATE TABLE seasons ( -id INTEGER NOT NULL PRIMARY KEY, -show_trakt_id INTEGER NOT NULL, -season_number INTEGER NOT NULL, -episode_count INTEGER NOT NULL, -name TEXT NOT NULL, -overview TEXT, -FOREIGN KEY (show_trakt_id) REFERENCES show(trakt_id) +import com.thomaskioko.tvmaniac.db.Id; +import com.thomaskioko.tvmaniac.db.SeasonId; +import com.thomaskioko.tvmaniac.db.ShowId; + +CREATE TABLE season ( + id INTEGER AS Id NOT NULL PRIMARY KEY, + show_id INTEGER AS Id NOT NULL, + season_number INTEGER NOT NULL, + title TEXT NOT NULL, + episode_count INTEGER NOT NULL, + overview TEXT, + FOREIGN KEY(show_id) REFERENCES show(id) ); insertOrReplace: -INSERT OR REPLACE INTO seasons( -id, -show_trakt_id, -season_number, -episode_count, -name, -overview +INSERT OR REPLACE INTO season( + id, + show_id, + season_number, + episode_count, + title, + overview ) VALUES(?,?,?,?,?,?); -seasonById: -SELECT * -FROM seasons -WHERE seasons.show_trakt_id = ? AND season_number != 0 -ORDER BY season_number ASC; +seasonsByShowId: +SELECT + show.id AS show_id, + season.id AS season_id, + season.title AS season_title, + season.season_number +FROM + show +JOIN + season ON show.id = season.show_id +WHERE + show.id = ?; + +seasonEpisodeDetailsById: +SELECT + show.id AS show_id, + show.title AS show_title, + season.id AS season_id, + season.title AS season_title, + season.overview AS season_overview, + season.season_number, + season.episode_count, + episode.id AS episode_id, + episode.season_id AS episode_season_id, + episode.title AS episode_title, + episode.episode_number, + episode.overview, + episode.runtime, + episode.ratings, + episode.votes, + episode_image.image_url AS episode_image_url +FROM + show +INNER JOIN + season ON season.show_id = show.id +INNER JOIN + episode ON episode.season_id = season.id +LEFT OUTER JOIN + episode_image ON episode_image.id = episode.id +WHERE + show.id = ? +AND + season_number != 0 +ORDER BY + season.season_number, episode_number ASC; delete: -DELETE FROM seasons WHERE show_trakt_id = ?; +DELETE FROM season WHERE show_id = ?; deleteAll: -DELETE FROM seasons; +DELETE FROM season; diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show.sq index 6d59b4b4d..36c234fce 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show.sq @@ -1,68 +1,90 @@ -import kotlin.collections.List; +import com.thomaskioko.tvmaniac.db.Id; +import com.thomaskioko.tvmaniac.db.ShowId; import kotlin.String; +import kotlin.collections.List; CREATE TABLE show( -trakt_id INTEGER NOT NULL PRIMARY KEY, -tmdb_id INTEGER, -title TEXT NOT NULL, -overview TEXT NOT NULL, -language TEXT, -year TEXT NOT NULL, -rating REAL NOT NULL, -status TEXT NOT NULL, -runtime INTEGER NOT NULL, -votes INTEGER NOT NULL, -aired_episodes INTEGER DEFAULT NULL, -genres TEXT AS List NOT NULL + id INTEGER AS Id NOT NULL PRIMARY KEY, + tmdb_id INTEGER, + title TEXT NOT NULL, + overview TEXT NOT NULL, + language TEXT, + year TEXT NOT NULL, + rating REAL NOT NULL, + status TEXT NOT NULL, + runtime INTEGER NOT NULL, + votes INTEGER NOT NULL, + aired_episodes INTEGER DEFAULT NULL, + genres TEXT AS List NOT NULL ); insertOrReplace: INSERT OR REPLACE INTO show( -trakt_id, -tmdb_id, -title, -overview, -language, -year, -votes, -runtime, -rating, -genres, -status + id, + tmdb_id, + title, + overview, + language, + year, + votes, + runtime, + rating, + genres, + status ) VALUES(?,?,?,?,?,?,?,?,?,?,?); showById: -SELECT * FROM show -LEFT OUTER JOIN show_image ON show_image.trakt_id = show.trakt_id -LEFT OUTER JOIN watchlist ON show.trakt_id = watchlist.id -WHERE show.trakt_id = ?; - -selectShowByTmdbId: -SELECT * FROM show -WHERE tmdb_id = ?; +SELECT + show.id, + show.tmdb_id , + show.title, + show.overview, + show.language, + show.year, + show.rating, + show.votes, + show.status, + show.runtime, + show.genres, + show.aired_episodes, + show_image.poster_url, + show_image.backdrop_url, + CASE WHEN watchlist.id IS NOT NULL THEN 1 ELSE 0 END AS in_watchlist +FROM + show +LEFT OUTER JOIN + show_image ON show_image.id = show.id +LEFT OUTER JOIN + watchlist ON show.id = watchlist.id +WHERE + show.id = ?; shows: -SELECT show.trakt_id, show.tmdb_id ,show.title, show.overview, show.language, show.year, show.rating, -show.votes, show.status, show.runtime, show.genres, show.aired_episodes, show_image.poster_url, show_image.backdrop_url, -show_category.category_id -FROM show -INNER JOIN show_category ON show_category.trakt_id = show.trakt_id -LEFT OUTER JOIN show_image ON show_image.trakt_id = show.trakt_id -ORDER BY show.rating, show.year DESC; - -showsByCategory: -SELECT show.trakt_id, show.tmdb_id ,show.title, show.overview, show.language, show.year, show.rating, -show.votes, show.status, show.runtime, show.genres, show.aired_episodes, show_image.poster_url, show_image.backdrop_url, -show_category.category_id -FROM show -INNER JOIN show_category ON show_category.trakt_id = show.trakt_id -LEFT OUTER JOIN show_image ON show_image.trakt_id = show.trakt_id -WHERE show_category.category_id = ? -ORDER BY show.rating, show.year DESC; - -delete: -DELETE FROM show WHERE trakt_id = ?; +SELECT + show.id, + show.tmdb_id , + show.title, + show.overview, + show.language, + show.year, + show.rating, + show.votes, + show.status, + show.runtime, + show.genres, + show.aired_episodes, + show_image.poster_url, + show_image.backdrop_url, + show_category.category_id +FROM + show +JOIN + show_category ON show_category.id = show.id +LEFT OUTER JOIN + show_image ON show_image.id = show.id +ORDER BY + show.rating, show.year DESC; deleteAll: DELETE FROM show; \ No newline at end of file diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show_category.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show_category.sq index 5bd008c92..1f01b9862 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show_category.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show_category.sq @@ -1,12 +1,45 @@ +import com.thomaskioko.tvmaniac.db.CategoryId; +import com.thomaskioko.tvmaniac.db.Id; +import com.thomaskioko.tvmaniac.db.ShowId; + CREATE TABLE show_category ( -trakt_id INTEGER DEFAULT NULL PRIMARY KEY, -category_id INTEGER DEFAULT NULL, -FOREIGN KEY (trakt_id) REFERENCES show(trakt_id) + id INTEGER AS Id DEFAULT NULL, + category_id INTEGER AS Id DEFAULT NULL, + FOREIGN KEY(id) REFERENCES show(id), + PRIMARY KEY(id, category_id) ); insertOrReplace: INSERT OR REPLACE INTO show_category( -trakt_id, -category_id + id, + category_id ) -VALUES(?,?); \ No newline at end of file +VALUES(?,?); + +showsByCategory: +SELECT + show.id, + show.tmdb_id , + show.title, + show.overview, + show.language, + show.year, + show.rating, + show.votes, + show.status, + show.runtime, + show.genres, + show.aired_episodes, + show_image.poster_url, + show_image.backdrop_url, + show_category.category_id +FROM + show +INNER JOIN + show_category ON show_category.id = show.id +LEFT OUTER JOIN + show_image ON show_image.id = show.id +WHERE + show_category.category_id = ? +ORDER BY + show.rating, show.year DESC; \ No newline at end of file diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show_image.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show_image.sq index 54a9c127f..fe7c3b00e 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show_image.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/show_image.sq @@ -1,31 +1,39 @@ +import com.thomaskioko.tvmaniac.db.Id; +import com.thomaskioko.tvmaniac.db.ShowId; + CREATE TABLE show_image ( -trakt_id INTEGER NOT NULL PRIMARY KEY, -tmdb_id INTEGER DEFAULT NULL, -poster_url TEXT DEFAULT NULL, -backdrop_url TEXT DEFAULT NULL, -FOREIGN KEY (trakt_id) REFERENCES show(trakt_id) ON DELETE CASCADE + id INTEGER AS Id NOT NULL PRIMARY KEY, + tmdb_id INTEGER DEFAULT NULL, + poster_url TEXT DEFAULT NULL, + backdrop_url TEXT DEFAULT NULL, + FOREIGN KEY(id) REFERENCES show(id) ON DELETE CASCADE, + UNIQUE(id) ); insertOrReplace: INSERT OR REPLACE INTO show_image( -trakt_id, -tmdb_id, -poster_url, -backdrop_url + id, + tmdb_id, + poster_url, + backdrop_url ) VALUES(?,?,?,?); -selectImages: -SELECT * FROM show_image -WHERE poster_url IS NULL; - -selectShowImages: -SELECT show.trakt_id, show.tmdb_id FROM show -LEFT OUTER JOIN show_image ON show_image.trakt_id = show.trakt_id -WHERE poster_url IS NULL AND backdrop_url IS NULL; +emptyShowImage: +SELECT + show.id, + show.tmdb_id, + show_image.poster_url, + show_image.backdrop_url +FROM + show +LEFT OUTER JOIN + show_image ON show_image.id = show.id +WHERE + poster_url IS NULL AND backdrop_url IS NULL; delete: -DELETE FROM show_image WHERE trakt_id = ?; +DELETE FROM show_image WHERE id = ?; deleteAll: DELETE FROM show_image; \ No newline at end of file diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/similar_shows.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/similar_shows.sq index fa61f1337..ed4637853 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/similar_shows.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/similar_shows.sq @@ -1,26 +1,48 @@ +import com.thomaskioko.tvmaniac.db.Id; +import com.thomaskioko.tvmaniac.db.ShowId; +import com.thomaskioko.tvmaniac.db.SimilarShowId; + CREATE TABLE similar_shows ( -id INTEGER DEFAULT NULL PRIMARY KEY, -trakt_id INTEGER NOT NULL, -FOREIGN KEY (trakt_id) REFERENCES show(trakt_id) + id INTEGER AS Id, + similar_show_id INTEGER AS Id NOT NULL, + PRIMARY KEY (similar_show_id, id), + FOREIGN KEY(similar_show_id) REFERENCES show(id), + FOREIGN KEY(id) REFERENCES show(id) ); insertOrReplace: INSERT OR REPLACE INTO similar_shows( -id, -trakt_id + id, + similar_show_id ) VALUES(?,?); similarShows: -SELECT show.trakt_id, show.tmdb_id ,show.title, show.overview, show.language, show.year, show.rating, show.votes, show.status, -show.runtime, show.genres, show_image.poster_url, show_image.backdrop_url -FROM show -JOIN similar_shows ON show.trakt_id = similar_shows.id -INNER JOIN show_image ON show_image.trakt_id = similar_shows.id -WHERE similar_shows.trakt_id = ?; +SELECT + show.id, + show.tmdb_id , + show.title, + show.overview, + show.language, + show.year, + show.rating, + show.votes, + show.status, + show.runtime, + show.genres, + show_image.poster_url, + show_image.backdrop_url +FROM + show +JOIN + similar_shows ON show.id = similar_shows.id +INNER JOIN + show_image ON show_image.id = similar_shows.id +WHERE + similar_shows.similar_show_id = ?; delete: -DELETE FROM similar_shows WHERE trakt_id = ?; +DELETE FROM similar_shows WHERE similar_show_id = ?; deleteAll: DELETE FROM similar_shows; \ No newline at end of file diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/stats.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/stats.sq index e51acf2f8..28b9ff4bc 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/stats.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/stats.sq @@ -1,20 +1,20 @@ CREATE TABLE stats( -slug TEXT NOT NULL PRIMARY KEY, -months Text NOT NULL, -days Text NOT NULL, -hours Text NOT NULL, -collected_shows Text NOT NULL, -episodes_watched Text NOT NULL + slug TEXT NOT NULL PRIMARY KEY, + months Text NOT NULL, + days Text NOT NULL, + hours Text NOT NULL, + collected_shows Text NOT NULL, + episodes_watched Text NOT NULL ); insertOrReplace: INSERT OR REPLACE INTO stats( -slug, -months, -days, -hours, -collected_shows, -episodes_watched + slug, + months, + days, + hours, + collected_shows, + episodes_watched ) VALUES(?,?,?,?,?,?); diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/trailers.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/trailers.sq index e94540844..171f4ebcf 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/trailers.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/trailers.sq @@ -1,33 +1,36 @@ +import com.thomaskioko.tvmaniac.db.Id; +import com.thomaskioko.tvmaniac.db.ShowId; + CREATE TABLE trailers ( -id TEXT NOT NULL PRIMARY KEY, -trakt_id INTEGER NOT NULL, -key TEXT NOT NULL, -name TEXT NOT NULL, -site TEXT NOT NULL, -size INTEGER NOT NULL, -type TEXT NOT NULL, -FOREIGN KEY (trakt_id) REFERENCES show(trakt_id) + id TEXT NOT NULL PRIMARY KEY, + show_id INTEGER AS Id NOT NULL, + key TEXT NOT NULL, + name TEXT NOT NULL, + site TEXT NOT NULL, + size INTEGER NOT NULL, + type TEXT NOT NULL, + FOREIGN KEY(show_id)REFERENCES show(id) ); insertOrReplace: INSERT OR REPLACE INTO trailers( -id, -trakt_id, -key, -name, -site, -size, -type + id, + show_id, + key, + name, + site, + size, + type ) VALUES(?,?,?,?,?,?,?); selectByShowId: SELECT * FROM trailers -WHERE trakt_id = ?; +WHERE show_id = ?; delete: -DELETE FROM trailers WHERE trakt_id = ?; +DELETE FROM trailers WHERE show_id = ?; deleteAll: DELETE FROM trailers; \ No newline at end of file diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/trakt_shows_list.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/trakt_shows_list.sq index 292d0a3fe..eb3f25b61 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/trakt_shows_list.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/trakt_shows_list.sq @@ -1,14 +1,14 @@ CREATE TABLE trakt_shows_list ( -id INTEGER NOT NULL PRIMARY KEY, -slug TEXT NOT NULL, -description TEXT NOT NULL + id INTEGER NOT NULL PRIMARY KEY, + slug TEXT NOT NULL, + description TEXT NOT NULL ); insertOrReplace: INSERT OR REPLACE INTO trakt_shows_list( -id, -slug, -description + id, + slug, + description ) VALUES(?,?,?); diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/user.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/user.sq index 08aebafd3..a55bbb7c0 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/user.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/user.sq @@ -1,35 +1,43 @@ import kotlin.Boolean; CREATE TABLE user ( -slug TEXT NOT NULL PRIMARY KEY, -user_name Text NOT NULL, -full_name TEXT, -profile_picture TEXT, -is_me INTEGER AS Boolean NOT NULL DEFAULT 0 + slug TEXT NOT NULL PRIMARY KEY, + user_name Text NOT NULL, + full_name TEXT, + profile_picture TEXT, + is_me INTEGER AS Boolean NOT NULL DEFAULT 0 ); insertOrReplace: INSERT OR REPLACE INTO user( -slug, -user_name, -full_name, -profile_picture, -is_me + slug, + user_name, + full_name, + profile_picture, + is_me ) VALUES(?,?,?,?,? ); userBySlug: -SELECT * -FROM user -WHERE slug = ?; +SELECT + * +FROM + user +WHERE + slug = ?; getCurrentUser: -SELECT * -FROM user -WHERE is_me != 0; +SELECT + * +FROM + user +WHERE + is_me != 0; delete: -DELETE FROM user WHERE slug = ?; +DELETE FROM + user +WHERE slug = ?; deleteAll: DELETE FROM user; \ No newline at end of file diff --git a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/watchlist.sq b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/watchlist.sq index d2e89de4c..ccb4cc502 100644 --- a/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/watchlist.sq +++ b/core/database/src/commonMain/sqldelight/com/thomaskioko/tvmaniac/core/db/watchlist.sq @@ -1,36 +1,67 @@ +import com.thomaskioko.tvmaniac.db.Id; +import com.thomaskioko.tvmaniac.db.ShowId; import kotlin.Boolean; CREATE TABLE watchlist ( -id INTEGER PRIMARY KEY, -synced INTEGER AS Boolean NOT NULL DEFAULT 0, -created_at INTEGER NOT NULL, -FOREIGN KEY (id) REFERENCES show(trakt_id) ON DELETE CASCADE + id INTEGER AS Id PRIMARY KEY, + synced INTEGER AS Boolean NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + FOREIGN KEY(id) REFERENCES show(id) ON DELETE CASCADE, + UNIQUE(id) ); insertOrReplace: INSERT OR REPLACE INTO watchlist( -id, -synced, -created_at + id, + synced, + created_at ) -VALUES(?, ?, ?); +VALUES( ?, ?, ?); -selectUnsyncedShows: -SELECT * FROM watchlist -WHERE synced != 1; +unsyncedShows: +SELECT + * +FROM + watchlist +WHERE + synced != 1; -selectWatchlist: -SELECT * -FROM watchlist -INNER JOIN show ON show.trakt_id = watchlist.id -INNER JOIN show_image ON show_image.trakt_id = watchlist.id -ORDER BY created_at DESC; - -removeShow: -DELETE FROM watchlist -WHERE id = ?; +watchedShow: +SELECT + show.id AS show_id, + show.tmdb_id, + show.title, + show.overview, + show.language, + show.year, + show.rating, + show.votes, + show.status, + show.runtime, + show.genres, + show.aired_episodes, + show_image.poster_url, + show_image.backdrop_url, + watchlist.created_at +FROM + show +JOIN + watchlist ON show.id = watchlist.id +INNER JOIN + show_image ON show_image.id = watchlist.id +ORDER BY + watchlist.created_at DESC; updateFollowedState: -UPDATE watchlist -SET synced = ? -WHERE id = ?; \ No newline at end of file +UPDATE + watchlist +SET + synced = ? +WHERE + id = ?; + +removeShowFromWatchlist: +DELETE FROM + watchlist +WHERE + id = ?; \ No newline at end of file diff --git a/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/BaseDatabaseTest.kt b/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/BaseDatabaseTest.kt index 55b8c55ea..d009bd850 100644 --- a/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/BaseDatabaseTest.kt +++ b/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/BaseDatabaseTest.kt @@ -1,6 +1,7 @@ package com.thomaskioko.tvmaniac.core.db import app.cash.sqldelight.db.SqlDriver +import com.thomaskioko.tvmaniac.db.IdAdapter import com.thomaskioko.tvmaniac.db.InstantColumnAdapter import com.thomaskioko.tvmaniac.db.stringColumnAdapter import kotlin.test.AfterTest @@ -18,10 +19,40 @@ abstract class BaseDatabaseTest { driver = sqlDriver, showAdapter = Show.Adapter( genresAdapter = stringColumnAdapter, + idAdapter = IdAdapter(), ), last_requestsAdapter = Last_requests.Adapter( timestampAdapter = InstantColumnAdapter, ), + episode_imageAdapter = Episode_image.Adapter( + idAdapter = IdAdapter(), + tmdb_idAdapter = IdAdapter(), + ), + episodeAdapter = Episode.Adapter( + idAdapter = IdAdapter(), + season_idAdapter = IdAdapter(), + ), + seasonAdapter = Season.Adapter( + idAdapter = IdAdapter(), + show_idAdapter = IdAdapter(), + ), + show_imageAdapter = Show_image.Adapter( + idAdapter = IdAdapter(), + ), + similar_showsAdapter = Similar_shows.Adapter( + idAdapter = IdAdapter(), + similar_show_idAdapter = IdAdapter(), + ), + watchlistAdapter = Watchlist.Adapter( + idAdapter = IdAdapter(), + ), + show_categoryAdapter = Show_category.Adapter( + idAdapter = IdAdapter(), + category_idAdapter = IdAdapter(), + ), + trailersAdapter = Trailers.Adapter( + show_idAdapter = IdAdapter(), + ), ) @AfterTest diff --git a/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/EpisodesCacheTest.kt b/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/EpisodesCacheTest.kt index e31efd02d..602b0ad12 100644 --- a/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/EpisodesCacheTest.kt +++ b/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/EpisodesCacheTest.kt @@ -1,6 +1,7 @@ package com.thomaskioko.tvmaniac.core.db import com.thomaskioko.tvmaniac.core.db.MockData.getEpisodeCacheList +import com.thomaskioko.tvmaniac.db.Id import io.kotest.matchers.shouldBe import kotlin.test.Test @@ -13,9 +14,9 @@ internal class EpisodesCacheTest : BaseDatabaseTest() { getEpisodeCacheList().insertEpisodeEntityQuery() val entity = getEpisodeCacheList().first() - val queryResult = episodeQueries.episodeById(2534997).executeAsOne() + val queryResult = episodeQueries.episodesById(Id(2534997)).executeAsOne() - queryResult.trakt_id shouldBe entity.trakt_id + queryResult.id.id shouldBe entity.id.id queryResult.season_id shouldBe entity.season_id queryResult.title shouldBe entity.title queryResult.overview shouldBe entity.overview @@ -23,13 +24,13 @@ internal class EpisodesCacheTest : BaseDatabaseTest() { queryResult.votes shouldBe entity.votes } - private fun List.insertEpisodeEntityQuery() { + private fun List.insertEpisodeEntityQuery() { map { it.insertEpisodeEntityQuery() } } - private fun Episodes.insertEpisodeEntityQuery() { + private fun Episode.insertEpisodeEntityQuery() { episodeQueries.insertOrReplace( - trakt_id = trakt_id, + id = id, season_id = season_id, title = title, overview = overview, diff --git a/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/MockData.kt b/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/MockData.kt index 110e51607..89fbcbd40 100644 --- a/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/MockData.kt +++ b/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/MockData.kt @@ -1,13 +1,14 @@ package com.thomaskioko.tvmaniac.core.db -import com.thomaskioko.tvmaniac.core.db.Episodes as EpisodeCache +import com.thomaskioko.tvmaniac.db.Id +import com.thomaskioko.tvmaniac.core.db.Episode as EpisodeCache object MockData { fun getEpisodeCacheList() = listOf( EpisodeCache( - trakt_id = 2534997, - season_id = 114355, + id = Id(2534997), + season_id = Id(114355), title = "Glorious Purpose", overview = "After stealing the Tesseract in Avengers: Endgame, Loki lands before the Time Variance Authority.", votes = 42, @@ -17,8 +18,8 @@ object MockData { tmdb_id = 1, ), EpisodeCache( - trakt_id = 2927202, - season_id = 114355, + id = Id(2927202), + season_id = Id(114355), title = "The Variant", overview = "Mobius puts Loki to work, but not everyone at TVA is thrilled about the God of Mischief's presence.", votes = 23, @@ -30,7 +31,7 @@ object MockData { ) fun getShow() = Show( - trakt_id = 84958, + id = Id(84958), title = "Loki", overview = "After stealing the Tesseract during the events of “Avengers: Endgame,” " + "an alternate version of Loki is brought to the mysterious Time Variance " + @@ -51,7 +52,7 @@ object MockData { fun showList() = listOf( Show( - trakt_id = 84958, + id = Id(84958), title = "Loki", overview = "After stealing the Tesseract during the events of “Avengers: Endgame,” " + "an alternate version of Loki is brought to the mysterious Time Variance " + @@ -70,7 +71,7 @@ object MockData { aired_episodes = 12, ), Show( - trakt_id = 126280, + id = Id(126280), title = "Sex/Life", overview = "A woman's daring sexual past collides with her married-with-kids " + "present when the bad-boy ex she can't stop fantasizing about crashes " + @@ -88,7 +89,7 @@ object MockData { ) fun showCategory(traktId: Long, categoryId: Long) = Show_category( - trakt_id = traktId, - category_id = categoryId, + id = Id(traktId), + category_id = Id(categoryId), ) } diff --git a/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/TvShowCacheTest.kt b/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/TvShowCacheTest.kt index 425b5beec..95ab895d1 100644 --- a/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/TvShowCacheTest.kt +++ b/core/database/src/commonTest/kotlin/com/thomaskioko/tvmaniac/core/db/TvShowCacheTest.kt @@ -19,7 +19,7 @@ internal class TvShowCacheTest : BaseDatabaseTest() { shows.insertTvShowsEntityList() for (show in shows) { - showCategory(show.trakt_id, 1).insertCategory() + showCategory(show.id.id, 1).insertCategory() } val entities = tvShowQueries.shows().executeAsList() @@ -31,7 +31,7 @@ internal class TvShowCacheTest : BaseDatabaseTest() { fun verify_selectByShowId_returnTvShowEntity_afterInsertHasBeenDone() { getShow().insertTvShowQuery() - val entity = tvShowQueries.showById(getShow().trakt_id) + val entity = tvShowQueries.showById(getShow().id) .executeAsOne() entity shouldNotBe null @@ -48,7 +48,7 @@ internal class TvShowCacheTest : BaseDatabaseTest() { tvShowQueries.deleteAll() - val entity = tvShowQueries.showById(getShow().trakt_id) + val entity = tvShowQueries.showById(getShow().id) .executeAsOneOrNull() entity shouldBe null @@ -60,7 +60,7 @@ internal class TvShowCacheTest : BaseDatabaseTest() { private fun Show.insertTvShowQuery() { tvShowQueries.insertOrReplace( - trakt_id = trakt_id, + id = id, title = title, overview = overview, language = language, @@ -76,7 +76,7 @@ internal class TvShowCacheTest : BaseDatabaseTest() { private fun Show_category.insertCategory() { showCategoryQueries.insertOrReplace( - trakt_id = trakt_id, + id = id, category_id = category_id, ) } diff --git a/core/database/src/iosMain/kotlin/com/thomaskioko/tvmaniac/db/DatabaseComponent.kt b/core/database/src/iosMain/kotlin/com/thomaskioko/tvmaniac/db/DatabaseComponent.kt deleted file mode 100644 index 2c2d1e0b3..000000000 --- a/core/database/src/iosMain/kotlin/com/thomaskioko/tvmaniac/db/DatabaseComponent.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.thomaskioko.tvmaniac.db - -import app.cash.sqldelight.db.SqlDriver -import app.cash.sqldelight.driver.native.NativeSqliteDriver -import com.thomaskioko.tvmaniac.core.db.Last_requests -import com.thomaskioko.tvmaniac.core.db.Show -import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase -import com.thomaskioko.tvmaniac.util.scope.ApplicationScope -import me.tatarka.inject.annotations.Provides - -actual interface DatabaseComponent { - - @ApplicationScope - @Provides - fun provideSqlDriver(): SqlDriver = NativeSqliteDriver(TvManiacDatabase.Schema, "tvShows.db") - - @ApplicationScope - @Provides - fun provideTvManiacDatabase( - sqlDriver: SqlDriver, - ): TvManiacDatabase = TvManiacDatabase( - driver = sqlDriver, - showAdapter = Show.Adapter( - genresAdapter = stringColumnAdapter, - ), - last_requestsAdapter = Last_requests.Adapter( - timestampAdapter = InstantColumnAdapter, - ), - ) -} diff --git a/core/database/src/iosMain/kotlin/com/thomaskioko/tvmaniac/db/DatabasePlatformComponent.kt b/core/database/src/iosMain/kotlin/com/thomaskioko/tvmaniac/db/DatabasePlatformComponent.kt new file mode 100644 index 000000000..e1816c660 --- /dev/null +++ b/core/database/src/iosMain/kotlin/com/thomaskioko/tvmaniac/db/DatabasePlatformComponent.kt @@ -0,0 +1,14 @@ +package com.thomaskioko.tvmaniac.db + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.native.NativeSqliteDriver +import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase +import com.thomaskioko.tvmaniac.util.scope.ApplicationScope +import me.tatarka.inject.annotations.Provides + +actual interface DatabasePlatformComponent { + + @ApplicationScope + @Provides + fun provideSqlDriver(): SqlDriver = NativeSqliteDriver(TvManiacDatabase.Schema, "tvShows.db") +} diff --git a/core/database/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/db/DatabaseComponent.kt b/core/database/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/db/DatabaseComponent.kt deleted file mode 100644 index 312227327..000000000 --- a/core/database/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/db/DatabaseComponent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.thomaskioko.tvmaniac.db - -actual interface DatabaseComponent diff --git a/core/database/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/db/DatabasePlatformComponent.kt b/core/database/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/db/DatabasePlatformComponent.kt new file mode 100644 index 000000000..952dcb6ec --- /dev/null +++ b/core/database/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/db/DatabasePlatformComponent.kt @@ -0,0 +1,3 @@ +package com.thomaskioko.tvmaniac.db + +actual interface DatabasePlatformComponent diff --git a/core/datastore/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt b/core/datastore/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt index dab2f748c..0e9996d76 100644 --- a/core/datastore/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt +++ b/core/datastore/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt @@ -3,7 +3,6 @@ package com.thomaskioko.tvmaniac.datastore.implementation import android.app.Application import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences -import com.thomaskioko.tvmaniac.datastore.api.DatastoreRepository import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope import com.thomaskioko.tvmaniac.util.scope.ApplicationScope import me.tatarka.inject.annotations.Provides @@ -19,8 +18,4 @@ actual interface DataStorePlatformComponent { coroutineScope = scope.io, producePath = { context.filesDir.resolve(dataStoreFileName).absolutePath }, ) - - @ApplicationScope - @Provides - fun provideDatastoreRepository(bind: DatastoreRepositoryImpl): DatastoreRepository = bind } diff --git a/core/datastore/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt b/core/datastore/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt index 09e6ef4be..9c911c7c5 100644 --- a/core/datastore/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt +++ b/core/datastore/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt @@ -1,3 +1,14 @@ package com.thomaskioko.tvmaniac.datastore.implementation +import com.thomaskioko.tvmaniac.datastore.api.DatastoreRepository +import com.thomaskioko.tvmaniac.util.scope.ApplicationScope +import me.tatarka.inject.annotations.Provides + expect interface DataStorePlatformComponent + +interface DataStoreComponent : DataStorePlatformComponent { + + @ApplicationScope + @Provides + fun provideDatastoreRepository(bind: DatastoreRepositoryImpl): DatastoreRepository = bind +} diff --git a/core/datastore/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt b/core/datastore/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt index b08b6606b..3bf7f2a80 100644 --- a/core/datastore/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt +++ b/core/datastore/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/datastore/implementation/DataStorePlatformComponent.kt @@ -2,7 +2,6 @@ package com.thomaskioko.tvmaniac.datastore.implementation import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences -import com.thomaskioko.tvmaniac.datastore.api.DatastoreRepository import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope import com.thomaskioko.tvmaniac.util.scope.ApplicationScope import kotlinx.cinterop.ExperimentalForeignApi @@ -32,8 +31,4 @@ actual interface DataStorePlatformComponent { requireNotNull(documentDirectory).path + "/$dataStoreFileName" }, ) - - @ApplicationScope - @Provides - fun provideDatastoreRepository(bind: DatastoreRepositoryImpl): DatastoreRepository = bind } diff --git a/core/networkutil/src/androidMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt b/core/networkutil/src/androidMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt deleted file mode 100644 index 6c16a51cb..000000000 --- a/core/networkutil/src/androidMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.thomaskioko.tvmaniac.core.networkutil.inject - -import com.thomaskioko.tvmaniac.core.networkutil.AndroidNetworkExceptionHandlerUtil -import com.thomaskioko.tvmaniac.core.networkutil.NetworkExceptionHandler -import com.thomaskioko.tvmaniac.util.scope.ApplicationScope -import me.tatarka.inject.annotations.Provides - -actual interface NetworkPlatformComponent { - - @ApplicationScope - @Provides - fun provideNetworkExceptionHandler( - bind: AndroidNetworkExceptionHandlerUtil, - ): NetworkExceptionHandler = bind -} diff --git a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/NetworkBoundResource.kt b/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/NetworkBoundResource.kt deleted file mode 100644 index e0290f6fb..000000000 --- a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/NetworkBoundResource.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.thomaskioko.tvmaniac.core.networkutil - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn - -inline fun networkBoundResult( - crossinline query: () -> Flow, - crossinline fetch: suspend () -> RequestType, - crossinline saveFetchResult: suspend (RequestType) -> Unit, - crossinline shouldFetch: (ResultType?) -> Boolean = { true }, - exceptionHandler: NetworkExceptionHandler, - coroutineDispatcher: CoroutineDispatcher, -) = flow> { - val data = query().first() - - if (shouldFetch(data)) { - try { - saveFetchResult(fetch()) - emit(Either.Right(query().first())) - } catch (e: Exception) { - emit(Either.Left(DefaultError(exceptionHandler.resolveError(e)))) - } - } else { - emit(Either.Right(query().first())) - } -}.catch { - emit(Either.Left(DefaultError(exceptionHandler.resolveError(it)))) -}.flowOn(coroutineDispatcher) diff --git a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt b/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt deleted file mode 100644 index 75bef7e04..000000000 --- a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.thomaskioko.tvmaniac.core.networkutil.inject - -expect interface NetworkPlatformComponent diff --git a/core/networkutil/src/iosMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt b/core/networkutil/src/iosMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt deleted file mode 100644 index 0f8f5b4eb..000000000 --- a/core/networkutil/src/iosMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.thomaskioko.tvmaniac.core.networkutil.inject - -import com.thomaskioko.tvmaniac.core.networkutil.IosExceptionHandler -import com.thomaskioko.tvmaniac.core.networkutil.NetworkExceptionHandler -import com.thomaskioko.tvmaniac.util.scope.ApplicationScope -import me.tatarka.inject.annotations.Provides - -actual interface NetworkPlatformComponent { - @ApplicationScope - @Provides - fun provideExceptionHandler(bind: IosExceptionHandler): NetworkExceptionHandler = bind -} diff --git a/core/networkutil/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt b/core/networkutil/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt deleted file mode 100644 index 5bea96170..000000000 --- a/core/networkutil/src/jvmMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/inject/NetworkPlatformComponent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.thomaskioko.tvmaniac.core.networkutil.inject - -actual interface NetworkPlatformComponent diff --git a/core/tmdb-api/api/build.gradle.kts b/core/tmdb-api/api/build.gradle.kts index 9d4ac4c41..cf71540da 100644 --- a/core/tmdb-api/api/build.gradle.kts +++ b/core/tmdb-api/api/build.gradle.kts @@ -8,7 +8,7 @@ kotlin { commonMain { dependencies { api(projects.core.database) - api(projects.core.networkutil) + api(projects.core.util) implementation(libs.ktor.serialization) } diff --git a/core/tmdb-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/TmdbNetworkDataSource.kt b/core/tmdb-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/TmdbNetworkDataSource.kt index fc55d52c4..41e1a03d5 100644 --- a/core/tmdb-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/TmdbNetworkDataSource.kt +++ b/core/tmdb-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/TmdbNetworkDataSource.kt @@ -1,10 +1,10 @@ package com.thomaskioko.tvmaniac.tmdb.api -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse import com.thomaskioko.tvmaniac.tmdb.api.model.EpisodesResponse import com.thomaskioko.tvmaniac.tmdb.api.model.ErrorResponse import com.thomaskioko.tvmaniac.tmdb.api.model.ShowDetailResponse import com.thomaskioko.tvmaniac.tmdb.api.model.TrailersResponse +import com.thomaskioko.tvmaniac.util.model.ApiResponse interface TmdbNetworkDataSource { diff --git a/core/tmdb-api/implementation/build.gradle.kts b/core/tmdb-api/implementation/build.gradle.kts index 2244ed949..a3f1192a9 100644 --- a/core/tmdb-api/implementation/build.gradle.kts +++ b/core/tmdb-api/implementation/build.gradle.kts @@ -20,6 +20,7 @@ kotlin { implementation(libs.kotlinInject.runtime) implementation(libs.ktor.core) + implementation(libs.ktor.logging) implementation(libs.ktor.negotiation) implementation(libs.ktor.serialization.json) implementation(libs.sqldelight.extensions) diff --git a/core/tmdb-api/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformComponent.kt b/core/tmdb-api/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformComponent.kt index a7feec22b..455994245 100644 --- a/core/tmdb-api/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformComponent.kt +++ b/core/tmdb-api/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformComponent.kt @@ -1,38 +1,12 @@ package com.thomaskioko.tvmaniac.tmdb.implementation -import com.thomaskioko.tvmaniac.tmdb.api.TmdbNetworkDataSource -import com.thomaskioko.tvmaniac.util.model.Configs import com.thomaskioko.tvmaniac.util.scope.ApplicationScope import io.ktor.client.engine.okhttp.OkHttp -import kotlinx.serialization.json.Json import me.tatarka.inject.annotations.Provides actual interface TmdbPlatformComponent { - @ApplicationScope - @Provides - fun provideTmdbJson(): TmdbJson = Json { - ignoreUnknownKeys = true - prettyPrint = true - encodeDefaults = true - } - @ApplicationScope @Provides fun provideTmdbHttpClientEngine(): TmdbHttpClientEngine = OkHttp.create() - - @ApplicationScope - @Provides - fun provideTmdbHttpClient( - configs: Configs, - json: TmdbJson, - httpClientEngine: TmdbHttpClientEngine, - ): TmdbHttpClient = tmdbHttpClient( - tmdbApiKey = configs.tmdbApiKey, - json = json, - httpClientEngine = httpClientEngine, - ) - - @Provides - fun provideTmdbService(bind: TmdbNetworkDataSourceImpl): TmdbNetworkDataSource = bind } diff --git a/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbClient.kt b/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbClient.kt index 30e8fba69..b8ac6d96e 100644 --- a/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbClient.kt +++ b/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbClient.kt @@ -1,10 +1,15 @@ package com.thomaskioko.tvmaniac.tmdb.implementation +import com.thomaskioko.tvmaniac.util.KermitLogger import io.ktor.client.HttpClient import io.ktor.client.engine.HttpClientEngine import io.ktor.client.plugins.DefaultRequest import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.EMPTY +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging import io.ktor.client.request.headers import io.ktor.http.HttpHeaders import io.ktor.http.URLProtocol @@ -14,9 +19,11 @@ import kotlinx.serialization.json.Json const val TIMEOUT_DURATION: Long = 60_000 fun tmdbHttpClient( + isDebug: Boolean = false, tmdbApiKey: String, json: Json, httpClientEngine: HttpClientEngine, + kermitLogger: KermitLogger, ) = HttpClient(httpClientEngine) { install(ContentNegotiation) { json(json = json) @@ -43,4 +50,17 @@ fun tmdbHttpClient( connectTimeoutMillis = TIMEOUT_DURATION socketTimeoutMillis = TIMEOUT_DURATION } + + install(Logging) { + level = LogLevel.INFO + logger = if (isDebug) { + object : Logger { + override fun log(message: String) { + kermitLogger.info("TmbdHttp", message) + } + } + } else { + Logger.EMPTY + } + } } diff --git a/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbComponent.kt b/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbComponent.kt new file mode 100644 index 000000000..d621cc0d6 --- /dev/null +++ b/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbComponent.kt @@ -0,0 +1,48 @@ +package com.thomaskioko.tvmaniac.tmdb.implementation + +import com.thomaskioko.tvmaniac.tmdb.api.TmdbNetworkDataSource +import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.model.Configs +import com.thomaskioko.tvmaniac.util.scope.ApplicationScope +import io.ktor.client.HttpClient +import io.ktor.client.engine.HttpClientEngine +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import me.tatarka.inject.annotations.Provides + +typealias TmdbHttpClient = HttpClient +typealias TmdbHttpClientEngine = HttpClientEngine +typealias TmdbJson = Json + +expect interface TmdbPlatformComponent + +interface TmdbComponent : TmdbPlatformComponent { + + @OptIn(ExperimentalSerializationApi::class) + @ApplicationScope + @Provides + fun provideTmdbJson(): TmdbJson = Json { + isLenient = true + ignoreUnknownKeys = true + useAlternativeNames = false + explicitNulls = false + } + + @ApplicationScope + @Provides + fun provideTmdbHttpClient( + configs: Configs, + json: TmdbJson, + httpClientEngine: TmdbHttpClientEngine, + logger: KermitLogger, + ): TmdbHttpClient = tmdbHttpClient( + tmdbApiKey = configs.tmdbApiKey, + json = json, + httpClientEngine = httpClientEngine, + kermitLogger = logger, + isDebug = configs.isDebug, + ) + + @Provides + fun provideTmdbService(bind: TmdbNetworkDataSourceImpl): TmdbNetworkDataSource = bind +} diff --git a/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbNetworkDataSourceImpl.kt b/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbNetworkDataSourceImpl.kt index cc8129c84..dc99aa1a3 100644 --- a/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbNetworkDataSourceImpl.kt +++ b/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbNetworkDataSourceImpl.kt @@ -1,12 +1,12 @@ package com.thomaskioko.tvmaniac.tmdb.implementation -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse -import com.thomaskioko.tvmaniac.core.networkutil.safeRequest import com.thomaskioko.tvmaniac.tmdb.api.TmdbNetworkDataSource import com.thomaskioko.tvmaniac.tmdb.api.model.EpisodesResponse import com.thomaskioko.tvmaniac.tmdb.api.model.ErrorResponse import com.thomaskioko.tvmaniac.tmdb.api.model.ShowDetailResponse import com.thomaskioko.tvmaniac.tmdb.api.model.TrailersResponse +import com.thomaskioko.tvmaniac.util.model.ApiResponse +import com.thomaskioko.tvmaniac.util.model.safeRequest import io.ktor.http.HttpMethod import io.ktor.http.path import me.tatarka.inject.annotations.Inject diff --git a/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformComponent.kt b/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformComponent.kt deleted file mode 100644 index a4fc2c2bc..000000000 --- a/core/tmdb-api/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformComponent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.thomaskioko.tvmaniac.tmdb.implementation - -import io.ktor.client.HttpClient -import io.ktor.client.engine.HttpClientEngine -import kotlinx.serialization.json.Json - -typealias TmdbHttpClient = HttpClient -typealias TmdbHttpClientEngine = HttpClientEngine -typealias TmdbJson = Json - -expect interface TmdbPlatformComponent diff --git a/core/tmdb-api/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformComponent.kt b/core/tmdb-api/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformComponent.kt index 9159ffdd6..930725e2d 100644 --- a/core/tmdb-api/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformComponent.kt +++ b/core/tmdb-api/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformComponent.kt @@ -1,41 +1,12 @@ package com.thomaskioko.tvmaniac.tmdb.implementation -import com.thomaskioko.tvmaniac.tmdb.api.TmdbNetworkDataSource -import com.thomaskioko.tvmaniac.util.model.Configs import com.thomaskioko.tvmaniac.util.scope.ApplicationScope import io.ktor.client.engine.darwin.Darwin -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json import me.tatarka.inject.annotations.Provides actual interface TmdbPlatformComponent { - @OptIn(ExperimentalSerializationApi::class) - @ApplicationScope - @Provides - fun provideTmdbJson(): TmdbJson = Json { - isLenient = true - ignoreUnknownKeys = true - useAlternativeNames = false - explicitNulls = false - } - @ApplicationScope @Provides fun provideTmdbHttpClientEngine(): TmdbHttpClientEngine = Darwin.create() - - @ApplicationScope - @Provides - fun provideTmdbHttpClient( - configs: Configs, - json: TmdbJson, - httpClientEngine: TmdbHttpClientEngine, - ): TmdbHttpClient = tmdbHttpClient( - json = json, - httpClientEngine = httpClientEngine, - tmdbApiKey = configs.tmdbApiKey, - ) - - @Provides - fun provideTmdbService(bind: TmdbNetworkDataSourceImpl): TmdbNetworkDataSource = bind } diff --git a/core/trakt-api/api/build.gradle.kts b/core/trakt-api/api/build.gradle.kts index 60fcd9da5..90d336850 100644 --- a/core/trakt-api/api/build.gradle.kts +++ b/core/trakt-api/api/build.gradle.kts @@ -7,7 +7,7 @@ kotlin { sourceSets { commonMain { dependencies { - api(projects.core.networkutil) + api(projects.core.util) implementation(libs.ktor.serialization) } } diff --git a/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktListRemoteDataSource.kt b/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktListRemoteDataSource.kt index 417b15265..b431f73b3 100644 --- a/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktListRemoteDataSource.kt +++ b/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktListRemoteDataSource.kt @@ -1,6 +1,5 @@ package com.thomaskioko.tvmaniac.trakt.api -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse import com.thomaskioko.tvmaniac.trakt.api.model.ErrorResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktAddRemoveShowFromListResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktAddShowToListResponse @@ -8,6 +7,7 @@ import com.thomaskioko.tvmaniac.trakt.api.model.TraktCreateListResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktFollowedShowResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktPersonalListsResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktUserResponse +import com.thomaskioko.tvmaniac.util.model.ApiResponse interface TraktListRemoteDataSource { diff --git a/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktShowsRemoteDataSource.kt b/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktShowsRemoteDataSource.kt index 07a9350da..b74d073a4 100644 --- a/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktShowsRemoteDataSource.kt +++ b/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktShowsRemoteDataSource.kt @@ -1,11 +1,11 @@ package com.thomaskioko.tvmaniac.trakt.api -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse import com.thomaskioko.tvmaniac.trakt.api.model.ErrorResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktSeasonEpisodesResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktSeasonsResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktShowResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktShowsResponse +import com.thomaskioko.tvmaniac.util.model.ApiResponse private const val DEFAULT_API_PAGE: Long = 1 private const val FETCH_PERIOD: String = "daily" diff --git a/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktStatsRemoteDataSource.kt b/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktStatsRemoteDataSource.kt index b58628730..3287ac69d 100644 --- a/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktStatsRemoteDataSource.kt +++ b/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktStatsRemoteDataSource.kt @@ -1,8 +1,8 @@ package com.thomaskioko.tvmaniac.trakt.api -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse import com.thomaskioko.tvmaniac.trakt.api.model.ErrorResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktUserStatsResponse +import com.thomaskioko.tvmaniac.util.model.ApiResponse interface TraktStatsRemoteDataSource { diff --git a/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktUserRemoteDataSource.kt b/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktUserRemoteDataSource.kt index 60702f1c6..bab0c6b76 100644 --- a/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktUserRemoteDataSource.kt +++ b/core/trakt-api/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktUserRemoteDataSource.kt @@ -1,9 +1,9 @@ package com.thomaskioko.tvmaniac.trakt.api -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse import com.thomaskioko.tvmaniac.trakt.api.model.ErrorResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktPersonalListsResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktUserResponse +import com.thomaskioko.tvmaniac.util.model.ApiResponse interface TraktUserRemoteDataSource { diff --git a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktHttpClient.kt b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktHttpClient.kt index 6a96376fa..056487169 100644 --- a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktHttpClient.kt +++ b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktHttpClient.kt @@ -2,8 +2,8 @@ package com.thomaskioko.trakt.service.implementation import com.thomaskioko.trakt.service.implementation.inject.TraktHttpClientEngine import com.thomaskioko.trakt.service.implementation.inject.TraktJson -import com.thomaskioko.tvmaniac.core.networkutil.HttpExceptions import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.model.HttpExceptions import io.ktor.client.HttpClient import io.ktor.client.plugins.DefaultRequest import io.ktor.client.plugins.HttpResponseValidator @@ -83,11 +83,11 @@ fun traktHttpClient( } install(Logging) { - level = LogLevel.BODY + level = LogLevel.INFO logger = if (isDebug) { object : Logger { override fun log(message: String) { - kermitLogger.debug(message) + kermitLogger.info("TraktHttp", message) } } } else { diff --git a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktListRemoteDataSourceImpl.kt b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktListRemoteDataSourceImpl.kt index d9db22d63..28294203f 100644 --- a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktListRemoteDataSourceImpl.kt +++ b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktListRemoteDataSourceImpl.kt @@ -1,8 +1,6 @@ package com.thomaskioko.trakt.service.implementation import com.thomaskioko.trakt.service.implementation.inject.TraktHttpClient -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse -import com.thomaskioko.tvmaniac.core.networkutil.safeRequest import com.thomaskioko.tvmaniac.trakt.api.TraktListRemoteDataSource import com.thomaskioko.tvmaniac.trakt.api.model.ErrorResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktAddRemoveShowFromListResponse @@ -15,6 +13,8 @@ import com.thomaskioko.tvmaniac.trakt.api.model.TraktPersonalListsResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktShow import com.thomaskioko.tvmaniac.trakt.api.model.TraktShowIds import com.thomaskioko.tvmaniac.trakt.api.model.TraktUserResponse +import com.thomaskioko.tvmaniac.util.model.ApiResponse +import com.thomaskioko.tvmaniac.util.model.safeRequest import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.client.request.parameter diff --git a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktShowsShowsRemoteDataSourceImpl.kt b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktShowsShowsRemoteDataSourceImpl.kt index 9399a7e93..02ea930c9 100644 --- a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktShowsShowsRemoteDataSourceImpl.kt +++ b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktShowsShowsRemoteDataSourceImpl.kt @@ -1,14 +1,14 @@ package com.thomaskioko.trakt.service.implementation import com.thomaskioko.trakt.service.implementation.inject.TraktHttpClient -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse -import com.thomaskioko.tvmaniac.core.networkutil.safeRequest import com.thomaskioko.tvmaniac.trakt.api.TraktShowsRemoteDataSource import com.thomaskioko.tvmaniac.trakt.api.model.ErrorResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktSeasonEpisodesResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktSeasonsResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktShowResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktShowsResponse +import com.thomaskioko.tvmaniac.util.model.ApiResponse +import com.thomaskioko.tvmaniac.util.model.safeRequest import io.ktor.client.request.parameter import io.ktor.http.HttpMethod import io.ktor.http.path diff --git a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktStatsRemoteDataSourceImpl.kt b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktStatsRemoteDataSourceImpl.kt index c4c2572ac..e648e5777 100644 --- a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktStatsRemoteDataSourceImpl.kt +++ b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktStatsRemoteDataSourceImpl.kt @@ -1,11 +1,11 @@ package com.thomaskioko.trakt.service.implementation import com.thomaskioko.trakt.service.implementation.inject.TraktHttpClient -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse -import com.thomaskioko.tvmaniac.core.networkutil.safeRequest import com.thomaskioko.tvmaniac.trakt.api.TraktStatsRemoteDataSource import com.thomaskioko.tvmaniac.trakt.api.model.ErrorResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktUserStatsResponse +import com.thomaskioko.tvmaniac.util.model.ApiResponse +import com.thomaskioko.tvmaniac.util.model.safeRequest import io.ktor.http.HttpMethod import io.ktor.http.path import me.tatarka.inject.annotations.Inject diff --git a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktUserRemoteDataSourceImpl.kt b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktUserRemoteDataSourceImpl.kt index 6b33c77f9..7e04c468e 100644 --- a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktUserRemoteDataSourceImpl.kt +++ b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktUserRemoteDataSourceImpl.kt @@ -1,12 +1,12 @@ package com.thomaskioko.trakt.service.implementation import com.thomaskioko.trakt.service.implementation.inject.TraktHttpClient -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse -import com.thomaskioko.tvmaniac.core.networkutil.safeRequest import com.thomaskioko.tvmaniac.trakt.api.TraktUserRemoteDataSource import com.thomaskioko.tvmaniac.trakt.api.model.ErrorResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktPersonalListsResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktUserResponse +import com.thomaskioko.tvmaniac.util.model.ApiResponse +import com.thomaskioko.tvmaniac.util.model.safeRequest import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.client.request.parameter diff --git a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/inject/TraktComponent.kt b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/inject/TraktComponent.kt index 44e786260..6f5f234ba 100644 --- a/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/inject/TraktComponent.kt +++ b/core/trakt-api/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/inject/TraktComponent.kt @@ -21,7 +21,7 @@ import me.tatarka.inject.annotations.Provides typealias TraktHttpClient = HttpClient typealias TraktJson = Json -interface TraktComponent { +interface TraktComponent : TraktPlatformComponent { @ApplicationScope @Provides diff --git a/core/networkutil/src/androidMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/AndroidNetworkExceptionHandlerUtil.kt b/core/util/src/androidMain/kotlin/com/thomaskioko/tvmaniac/util/AndroidNetworkExceptionHandlerUtil.kt similarity index 95% rename from core/networkutil/src/androidMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/AndroidNetworkExceptionHandlerUtil.kt rename to core/util/src/androidMain/kotlin/com/thomaskioko/tvmaniac/util/AndroidNetworkExceptionHandlerUtil.kt index 728098381..f99b2d385 100644 --- a/core/networkutil/src/androidMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/AndroidNetworkExceptionHandlerUtil.kt +++ b/core/util/src/androidMain/kotlin/com/thomaskioko/tvmaniac/util/AndroidNetworkExceptionHandlerUtil.kt @@ -1,4 +1,4 @@ -package com.thomaskioko.tvmaniac.core.networkutil +package com.thomaskioko.tvmaniac.util import com.thomaskioko.tvmaniac.util.model.Configs import io.ktor.client.call.NoTransformationFoundException diff --git a/core/util/src/androidMain/kotlin/com/thomaskioko/tvmaniac/util/inject/UtilPlatformComponent.kt b/core/util/src/androidMain/kotlin/com/thomaskioko/tvmaniac/util/inject/UtilPlatformComponent.kt index 9d16508c7..64a944220 100644 --- a/core/util/src/androidMain/kotlin/com/thomaskioko/tvmaniac/util/inject/UtilPlatformComponent.kt +++ b/core/util/src/androidMain/kotlin/com/thomaskioko/tvmaniac/util/inject/UtilPlatformComponent.kt @@ -3,10 +3,12 @@ package com.thomaskioko.tvmaniac.util.inject import com.thomaskioko.tvmaniac.util.AndroidAppUtils import com.thomaskioko.tvmaniac.util.AndroidDateUtil import com.thomaskioko.tvmaniac.util.AndroidFormatterUtil +import com.thomaskioko.tvmaniac.util.AndroidNetworkExceptionHandlerUtil import com.thomaskioko.tvmaniac.util.AppUtils import com.thomaskioko.tvmaniac.util.ClasspathResourceReader import com.thomaskioko.tvmaniac.util.DateFormatter import com.thomaskioko.tvmaniac.util.FormatterUtil +import com.thomaskioko.tvmaniac.util.NetworkExceptionHandler import com.thomaskioko.tvmaniac.util.ResourceReader import com.thomaskioko.tvmaniac.util.YamlResourceReader import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers @@ -59,4 +61,10 @@ actual interface UtilPlatformComponent { @ApplicationScope @Provides fun provideResourceReader(bind: ClasspathResourceReader): ResourceReader = bind + + @ApplicationScope + @Provides + fun provideNetworkExceptionHandler( + bind: AndroidNetworkExceptionHandlerUtil, + ): NetworkExceptionHandler = bind } diff --git a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/NetworkExceptionHandler.kt b/core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/NetworkExceptionHandler.kt similarity index 64% rename from core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/NetworkExceptionHandler.kt rename to core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/NetworkExceptionHandler.kt index 759880a89..f587c0e83 100644 --- a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/NetworkExceptionHandler.kt +++ b/core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/NetworkExceptionHandler.kt @@ -1,4 +1,4 @@ -package com.thomaskioko.tvmaniac.core.networkutil +package com.thomaskioko.tvmaniac.util interface NetworkExceptionHandler { diff --git a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/ApiResponse.kt b/core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/model/ApiResponse.kt similarity index 98% rename from core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/ApiResponse.kt rename to core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/model/ApiResponse.kt index 1491d65bb..b547ecb9f 100644 --- a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/ApiResponse.kt +++ b/core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/model/ApiResponse.kt @@ -1,4 +1,4 @@ -package com.thomaskioko.tvmaniac.core.networkutil +package com.thomaskioko.tvmaniac.util.model import io.ktor.client.HttpClient import io.ktor.client.call.body diff --git a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/Either.kt b/core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/model/Either.kt similarity index 53% rename from core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/Either.kt rename to core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/model/Either.kt index d03480de2..9f89dc382 100644 --- a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/Either.kt +++ b/core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/model/Either.kt @@ -1,13 +1,16 @@ -package com.thomaskioko.tvmaniac.core.networkutil +package com.thomaskioko.tvmaniac.util.model sealed class Either { data class Left(val error: L) : Either() - data class Right(val data: R?) : Either() + data class Right(val data: R) : Either() fun fold(lfn: (L) -> T, rfn: (R?) -> T): T = when (this) { is Left -> lfn(error) is Right -> rfn(data) } + + fun getOrNull(): R? = (this as? Right)?.data + fun getErrorOrNull(): L? = (this as? Left)?.error } diff --git a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/Failure.kt b/core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/model/Failure.kt similarity index 55% rename from core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/Failure.kt rename to core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/model/Failure.kt index 443bb8f4e..417ece0c4 100644 --- a/core/networkutil/src/commonMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/Failure.kt +++ b/core/util/src/commonMain/kotlin/com/thomaskioko/tvmaniac/util/model/Failure.kt @@ -1,4 +1,4 @@ -package com.thomaskioko.tvmaniac.core.networkutil +package com.thomaskioko.tvmaniac.util.model sealed class Failure( val throwable: Throwable, @@ -9,3 +9,8 @@ class DefaultError(val message: String?) : Failure( throwable = Throwable(message), errorMessage = message, ) + +data class ServerError(val message: String?) : Failure( + throwable = Throwable(message), + errorMessage = message, +) diff --git a/core/networkutil/src/iosMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/IosExceptionHandler.kt b/core/util/src/iosMain/kotlin/com/thomaskioko/tvmaniac/util/IosExceptionHandler.kt similarity index 95% rename from core/networkutil/src/iosMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/IosExceptionHandler.kt rename to core/util/src/iosMain/kotlin/com/thomaskioko/tvmaniac/util/IosExceptionHandler.kt index 647edda45..425a07614 100644 --- a/core/networkutil/src/iosMain/kotlin/com/thomaskioko/tvmaniac/core/networkutil/IosExceptionHandler.kt +++ b/core/util/src/iosMain/kotlin/com/thomaskioko/tvmaniac/util/IosExceptionHandler.kt @@ -1,4 +1,4 @@ -package com.thomaskioko.tvmaniac.core.networkutil +package com.thomaskioko.tvmaniac.util import com.thomaskioko.tvmaniac.util.model.Configs import io.ktor.client.call.NoTransformationFoundException diff --git a/core/util/src/iosMain/kotlin/com/thomaskioko/tvmaniac/util/inject/UtilPlatformComponent.kt b/core/util/src/iosMain/kotlin/com/thomaskioko/tvmaniac/util/inject/UtilPlatformComponent.kt index 9e04184c4..31f02ef7c 100644 --- a/core/util/src/iosMain/kotlin/com/thomaskioko/tvmaniac/util/inject/UtilPlatformComponent.kt +++ b/core/util/src/iosMain/kotlin/com/thomaskioko/tvmaniac/util/inject/UtilPlatformComponent.kt @@ -7,7 +7,9 @@ import com.thomaskioko.tvmaniac.util.DateFormatter import com.thomaskioko.tvmaniac.util.FormatterUtil import com.thomaskioko.tvmaniac.util.IosAppUtils import com.thomaskioko.tvmaniac.util.IosDateFormatter +import com.thomaskioko.tvmaniac.util.IosExceptionHandler import com.thomaskioko.tvmaniac.util.IosFormatterUtil +import com.thomaskioko.tvmaniac.util.NetworkExceptionHandler import com.thomaskioko.tvmaniac.util.ResourceReader import com.thomaskioko.tvmaniac.util.YamlResourceReader import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers @@ -66,4 +68,8 @@ actual interface UtilPlatformComponent { @ApplicationScope @Provides fun provideResourceReader(bind: BundleResourceReader): ResourceReader = bind + + @ApplicationScope + @Provides + fun provideExceptionHandler(bind: IosExceptionHandler): NetworkExceptionHandler = bind } diff --git a/data/category/api/build.gradle.kts b/data/category/api/build.gradle.kts index 2425bebf1..52cbdb79f 100644 --- a/data/category/api/build.gradle.kts +++ b/data/category/api/build.gradle.kts @@ -9,7 +9,7 @@ kotlin { commonMain { dependencies { api(projects.core.database) - api(projects.core.networkutil) + api(projects.core.util) implementation(projects.core.tmdbApi.api) api(libs.coroutines.core) diff --git a/data/category/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/category/api/cache/CategoryCache.kt b/data/category/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/category/api/cache/CategoryCache.kt index d4a34253d..845e783ee 100644 --- a/data/category/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/category/api/cache/CategoryCache.kt +++ b/data/category/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/category/api/cache/CategoryCache.kt @@ -3,6 +3,6 @@ package com.thomaskioko.tvmaniac.category.api.cache import com.thomaskioko.tvmaniac.core.db.Show_category interface CategoryCache { - fun insert(category: Show_category) - fun insert(category: List) + fun upsert(category: Show_category) + fun upsert(category: List) } diff --git a/data/category/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/category/implementation/CategoryCacheImpl.kt b/data/category/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/category/implementation/CategoryCacheImpl.kt index f7efa87b8..aea103b44 100644 --- a/data/category/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/category/implementation/CategoryCacheImpl.kt +++ b/data/category/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/category/implementation/CategoryCacheImpl.kt @@ -12,16 +12,16 @@ class CategoryCacheImpl( private val showCategoryQuery get() = database.show_categoryQueries - override fun insert(category: Show_category) { + override fun upsert(category: Show_category) { database.transaction { showCategoryQuery.insertOrReplace( - trakt_id = category.trakt_id, + id = category.id, category_id = category.category_id, ) } } - override fun insert(category: List) { - category.forEach { insert(it) } + override fun upsert(category: List) { + category.forEach { upsert(it) } } } diff --git a/data/episodeimages/api/build.gradle.kts b/data/episodeimages/api/build.gradle.kts index 5dd2099de..adc8836f5 100644 --- a/data/episodeimages/api/build.gradle.kts +++ b/data/episodeimages/api/build.gradle.kts @@ -7,7 +7,7 @@ kotlin { commonMain { dependencies { api(projects.core.util) - api(projects.core.networkutil) + api(projects.core.util) api(projects.core.database) api(libs.coroutines.core) diff --git a/data/episodeimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/api/EpisodeImageDao.kt b/data/episodeimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/api/EpisodeImageDao.kt index c74f48296..89cae219b 100644 --- a/data/episodeimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/api/EpisodeImageDao.kt +++ b/data/episodeimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/api/EpisodeImageDao.kt @@ -6,9 +6,15 @@ import kotlinx.coroutines.flow.Flow interface EpisodeImageDao { - fun insert(entity: Episode_image) + fun upsert(entity: Episode_image) - fun insert(list: List) + fun upsert(list: List) - fun observeEpisodeImage(): Flow> + fun observeEpisodeImage(showId: Long): Flow> + + fun getEpisodeImage(showId: Long): List + + fun delete(id: Long) + + fun deleteAll() } diff --git a/data/episodeimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/api/EpisodeImageRepository.kt b/data/episodeimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/api/EpisodeImageRepository.kt index 8d0ec4222..886c3e657 100644 --- a/data/episodeimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/api/EpisodeImageRepository.kt +++ b/data/episodeimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/api/EpisodeImageRepository.kt @@ -1,10 +1,10 @@ package com.thomaskioko.tvmaniac.episodeimages.api -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow interface EpisodeImageRepository { - fun updateEpisodeImage(): Flow> + fun updateEpisodeImage(traktId: Long): Flow> } diff --git a/data/episodeimages/implementation/build.gradle.kts b/data/episodeimages/implementation/build.gradle.kts index ddb8a4baf..7dac36ace 100644 --- a/data/episodeimages/implementation/build.gradle.kts +++ b/data/episodeimages/implementation/build.gradle.kts @@ -10,9 +10,12 @@ kotlin { dependencies { implementation(projects.core.tmdbApi.api) implementation(projects.data.episodeimages.api) + implementation(projects.data.requestManager.api) implementation(libs.kotlinInject.runtime) + implementation(libs.kotlinx.atomicfu) implementation(libs.sqldelight.extensions) + implementation(libs.store5) } } diff --git a/data/episodeimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/implementation/EpisodeImageDaoImpl.kt b/data/episodeimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/implementation/EpisodeImageDaoImpl.kt index ab8fbd80e..13b3dd320 100644 --- a/data/episodeimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/implementation/EpisodeImageDaoImpl.kt +++ b/data/episodeimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/implementation/EpisodeImageDaoImpl.kt @@ -5,6 +5,7 @@ import app.cash.sqldelight.coroutines.mapToList import com.thomaskioko.tvmaniac.core.db.EpisodeImage import com.thomaskioko.tvmaniac.core.db.Episode_image import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.episodeimages.api.EpisodeImageDao import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers import kotlinx.coroutines.flow.Flow @@ -18,22 +19,34 @@ class EpisodeImageDaoImpl( private val episodeQueries get() = database.episode_imageQueries - override fun insert(entity: Episode_image) { + override fun upsert(list: List) { + database.transaction { + list.forEach { upsert(it) } + } + } + + override fun upsert(entity: Episode_image) { episodeQueries.insertOrReplace( - trakt_id = entity.trakt_id, + id = entity.id, tmdb_id = entity.tmdb_id, image_url = entity.image_url, ) } - override fun insert(list: List) { - database.transaction { - list.map { insert(it) } - } - } - - override fun observeEpisodeImage(): Flow> = - episodeQueries.episodeImage() + override fun observeEpisodeImage(showId: Long): Flow> = + episodeQueries.episodeImage(Id(showId)) .asFlow() .mapToList(dispatchers.io) + + override fun getEpisodeImage(showId: Long): List = + episodeQueries.episodeImage(Id(showId)) + .executeAsList() + + override fun delete(id: Long) { + episodeQueries.delete(Id(id)) + } + + override fun deleteAll() { + episodeQueries.deleteAll() + } } diff --git a/data/episodeimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/implementation/EpisodeImageRepositoryImpl.kt b/data/episodeimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/implementation/EpisodeImageRepositoryImpl.kt index 94ee1a533..441bd3eda 100644 --- a/data/episodeimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/implementation/EpisodeImageRepositoryImpl.kt +++ b/data/episodeimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/implementation/EpisodeImageRepositoryImpl.kt @@ -1,62 +1,43 @@ package com.thomaskioko.tvmaniac.episodeimages.implementation -import com.thomaskioko.tvmaniac.core.db.Episode_image -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse -import com.thomaskioko.tvmaniac.core.networkutil.DefaultError -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure -import com.thomaskioko.tvmaniac.core.networkutil.NetworkExceptionHandler import com.thomaskioko.tvmaniac.episodeimages.api.EpisodeImageDao import com.thomaskioko.tvmaniac.episodeimages.api.EpisodeImageRepository -import com.thomaskioko.tvmaniac.tmdb.api.TmdbNetworkDataSource -import com.thomaskioko.tvmaniac.util.FormatterUtil -import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository +import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject +import org.mobilenativefoundation.store.store5.StoreReadRequest +import kotlin.time.Duration.Companion.hours +@OptIn(ExperimentalCoroutinesApi::class) @Inject class EpisodeImageRepositoryImpl( - private val tmdbNetworkDataSource: TmdbNetworkDataSource, + private val dispatchers: AppCoroutineDispatchers, + private val requestManagerRepository: RequestManagerRepository, + private val store: EpisodeImageStore, private val episodeImageDao: EpisodeImageDao, - private val formatterUtil: FormatterUtil, - private val logger: KermitLogger, - private val exceptionHandler: NetworkExceptionHandler, ) : EpisodeImageRepository { - override fun updateEpisodeImage(): Flow> = - episodeImageDao.observeEpisodeImage() - .map { episode -> - episode.forEach { episodeArt -> - episodeArt.tmdb_id?.let { tmdbId -> - val response = tmdbNetworkDataSource.getEpisodeDetails( - tmdbShow = tmdbId, - ssnNumber = episodeArt.season_number!!, - epNumber = episodeArt.episode_number.toLong(), - ) - - when (response) { - is ApiResponse.Success -> { - episodeImageDao.insert( - Episode_image( - trakt_id = episodeArt.trakt_id, - tmdb_id = response.body.id.toLong(), - image_url = response.body.imageUrl?.let { - formatterUtil.formatTmdbPosterPath(it) - }, - ), - ) - } - - is ApiResponse.Error -> { - logger.error("updateEpisodeArtWork", "$response") - } - } - } - } - - Either.Right(Unit) + override fun updateEpisodeImage(traktId: Long): Flow> = + episodeImageDao.observeEpisodeImage(traktId) + .flatMapLatest { + store.stream( + StoreReadRequest.cached( + key = traktId, + refresh = requestManagerRepository.isRequestExpired( + entityId = traktId, + requestType = "EPISODE_IMAGE", + threshold = 1.hours, + ), + ), + ) } - .catch { Either.Left(DefaultError(exceptionHandler.resolveError(it))) } + .flatMapLatest { flowOf(Either.Right(Unit)) } + .flowOn(dispatchers.io) } diff --git a/data/episodeimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/implementation/EpisodeImageStore.kt b/data/episodeimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/implementation/EpisodeImageStore.kt new file mode 100644 index 000000000..27c9fd74a --- /dev/null +++ b/data/episodeimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodeimages/implementation/EpisodeImageStore.kt @@ -0,0 +1,93 @@ +package com.thomaskioko.tvmaniac.episodeimages.implementation + +import com.thomaskioko.tvmaniac.core.db.EpisodeImage +import com.thomaskioko.tvmaniac.core.db.Episode_image +import com.thomaskioko.tvmaniac.db.DatabaseTransactionRunner +import com.thomaskioko.tvmaniac.db.Id +import com.thomaskioko.tvmaniac.episodeimages.api.EpisodeImageDao +import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository +import com.thomaskioko.tvmaniac.tmdb.api.TmdbNetworkDataSource +import com.thomaskioko.tvmaniac.util.FormatterUtil +import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.model.ApiResponse +import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope +import me.tatarka.inject.annotations.Inject +import org.mobilenativefoundation.store.store5.Fetcher +import org.mobilenativefoundation.store.store5.SourceOfTruth +import org.mobilenativefoundation.store.store5.Store +import org.mobilenativefoundation.store.store5.StoreBuilder +import org.mobilenativefoundation.store.store5.Validator +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours + +@Inject +class EpisodeImageStore( + private val tmdbNetworkDataSource: TmdbNetworkDataSource, + private val episodeImageDao: EpisodeImageDao, + private val requestManagerRepository: RequestManagerRepository, + private val dbTransactionRunner: DatabaseTransactionRunner, + private val formatterUtil: FormatterUtil, + private val logger: KermitLogger, + private val scope: AppCoroutineScope, +) : Store> by StoreBuilder.from( + fetcher = Fetcher.of { id -> + episodeImageDao.getEpisodeImage(id) + .filter { it.tmdb_id != null && it.image_url == null } + .map { episodeArt -> + val apiResponse = tmdbNetworkDataSource.getEpisodeDetails( + tmdbShow = episodeArt.tmdb_id!!, + ssnNumber = episodeArt.season_number!!, + epNumber = episodeArt.episode_number.toLong(), + ) + + when (apiResponse) { + is ApiResponse.Success -> { + Episode_image( + id = Id(id = episodeArt.id.id), + tmdb_id = Id(id = episodeArt.tmdb_id!!), + image_url = apiResponse.body.imageUrl?.let { + formatterUtil.formatTmdbPosterPath(it) + }, + ) + } + + is ApiResponse.Error -> { + logger.error("EpisodeImageStore", "$apiResponse") + throw Throwable("$apiResponse") + } + } + } + }, + sourceOfTruth = SourceOfTruth.of( + reader = { id -> episodeImageDao.observeEpisodeImage(id) }, + writer = { _, imageList -> + dbTransactionRunner { + episodeImageDao.upsert(imageList) + } + }, + delete = episodeImageDao::delete, + deleteAll = { dbTransactionRunner(episodeImageDao::deleteAll) }, + ), +).validator( + Validator.by { result -> + if (result.isNotEmpty()) { + requestManagerRepository.isRequestExpired( + entityId = result.first().tmdb_id!!, + requestType = "EPISODE_IMAGE", + threshold = 180.days, + ) + } else { + result.firstOrNull()?.tmdb_id?.let { + requestManagerRepository.isRequestExpired( + entityId = it, + requestType = "EPISODE_IMAGE", + threshold = 1.hours, + ) + } ?: run { + true + } + } + }, +) + .scope(scope.io) + .build() diff --git a/data/episodeimages/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/testing/FakeEpisodeImageRepository.kt b/data/episodeimages/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/testing/FakeEpisodeImageRepository.kt index b80927a73..84cbebc38 100644 --- a/data/episodeimages/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/testing/FakeEpisodeImageRepository.kt +++ b/data/episodeimages/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/testing/FakeEpisodeImageRepository.kt @@ -1,12 +1,14 @@ package com.thomaskioko.tvmaniac.episodes.testing -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure import com.thomaskioko.tvmaniac.episodeimages.api.EpisodeImageRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf class FakeEpisodeImageRepository : EpisodeImageRepository { - override fun updateEpisodeImage(): Flow> = flowOf(Either.Right(Unit)) + override fun updateEpisodeImage( + traktId: Long, + ): Flow> = flowOf(Either.Right(Unit)) } diff --git a/data/episodes/api/build.gradle.kts b/data/episodes/api/build.gradle.kts index 5dd2099de..862106859 100644 --- a/data/episodes/api/build.gradle.kts +++ b/data/episodes/api/build.gradle.kts @@ -7,7 +7,6 @@ kotlin { commonMain { dependencies { api(projects.core.util) - api(projects.core.networkutil) api(projects.core.database) api(libs.coroutines.core) diff --git a/data/episodes/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/api/EpisodesDao.kt b/data/episodes/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/api/EpisodesDao.kt index ab85ff44c..56f0aac79 100644 --- a/data/episodes/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/api/EpisodesDao.kt +++ b/data/episodes/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/api/EpisodesDao.kt @@ -1,6 +1,6 @@ package com.thomaskioko.tvmaniac.episodes.api -import com.thomaskioko.tvmaniac.core.db.Episodes as EpisodeCache +import com.thomaskioko.tvmaniac.core.db.Episode as EpisodeCache interface EpisodesDao { diff --git a/data/episodes/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/implementation/EpisodesDaoImpl.kt b/data/episodes/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/implementation/EpisodesDaoImpl.kt index e8e5e3f66..19848fdf0 100644 --- a/data/episodes/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/implementation/EpisodesDaoImpl.kt +++ b/data/episodes/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/episodes/implementation/EpisodesDaoImpl.kt @@ -1,9 +1,10 @@ package com.thomaskioko.tvmaniac.episodes.implementation import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.episodes.api.EpisodesDao import me.tatarka.inject.annotations.Inject -import com.thomaskioko.tvmaniac.core.db.Episodes as EpisodeCache +import com.thomaskioko.tvmaniac.core.db.Episode as EpisodeCache @Inject class EpisodesDaoImpl( @@ -15,7 +16,7 @@ class EpisodesDaoImpl( override fun insert(entity: EpisodeCache) { database.transaction { episodeQueries.insertOrReplace( - trakt_id = entity.trakt_id, + id = entity.id, season_id = entity.season_id, tmdb_id = entity.tmdb_id, title = entity.title, @@ -33,7 +34,7 @@ class EpisodesDaoImpl( } override fun delete(id: Long) { - episodeQueries.delete(id) + episodeQueries.delete(Id(id)) } override fun deleteAll() { diff --git a/data/profile/api/build.gradle.kts b/data/profile/api/build.gradle.kts index 978bdc459..30e94f05c 100644 --- a/data/profile/api/build.gradle.kts +++ b/data/profile/api/build.gradle.kts @@ -7,12 +7,9 @@ kotlin { commonMain { dependencies { api(projects.core.database) - api(projects.core.networkutil) api(projects.core.util) api(libs.coroutines.core) - api(libs.kotlinx.atomicfu) - api(libs.store5) } } } diff --git a/data/profile/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/api/ProfileRepository.kt b/data/profile/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/api/ProfileRepository.kt index b385f9fe1..fe67ad32a 100644 --- a/data/profile/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/api/ProfileRepository.kt +++ b/data/profile/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/api/ProfileRepository.kt @@ -1,10 +1,11 @@ package com.thomaskioko.tvmaniac.profile.api import com.thomaskioko.tvmaniac.core.db.User +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow -import org.mobilenativefoundation.store.store5.StoreReadResponse interface ProfileRepository { - fun observeProfile(slug: String): Flow> + fun observeProfile(slug: String): Flow> suspend fun clearProfile() } diff --git a/data/profile/implementation/build.gradle.kts b/data/profile/implementation/build.gradle.kts index bb0cf83f7..3993d1ef3 100644 --- a/data/profile/implementation/build.gradle.kts +++ b/data/profile/implementation/build.gradle.kts @@ -14,6 +14,8 @@ kotlin { implementation(libs.kotlinInject.runtime) implementation(libs.sqldelight.extensions) + implementation(libs.kotlinx.atomicfu) + implementation(libs.store5) } } } diff --git a/data/profile/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/implementation/ProfileRepositoryImpl.kt b/data/profile/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/implementation/ProfileRepositoryImpl.kt index 12b7122e0..9c61f157b 100644 --- a/data/profile/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/implementation/ProfileRepositoryImpl.kt +++ b/data/profile/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/implementation/ProfileRepositoryImpl.kt @@ -4,7 +4,14 @@ import com.thomaskioko.tvmaniac.core.db.User import com.thomaskioko.tvmaniac.profile.api.ProfileRepository import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.ExperimentalStoreApi @@ -12,15 +19,15 @@ import org.mobilenativefoundation.store.store5.StoreReadRequest import org.mobilenativefoundation.store.store5.StoreReadResponse import kotlin.time.Duration.Companion.days -@OptIn(ExperimentalStoreApi::class) +@OptIn(ExperimentalStoreApi::class, ExperimentalCoroutinesApi::class) @Inject -class ProfileRepositoryImpl constructor( +class ProfileRepositoryImpl( private val store: ProfileStore, private val requestManagerRepository: RequestManagerRepository, private val dispatchers: AppCoroutineDispatchers, ) : ProfileRepository { - override fun observeProfile(slug: String): Flow> = + override fun observeProfile(slug: String): Flow> = store.stream( StoreReadRequest.cached( key = slug, @@ -31,9 +38,21 @@ class ProfileRepositoryImpl constructor( ), ), ) - .flowOn(dispatchers.io) + .mapResult() override suspend fun clearProfile() { store.clear() } + + private fun Flow>.mapResult(): Flow> = + distinctUntilChanged() + .flatMapLatest { + val data = it.dataOrNull() + if (data != null) { + flowOf(Either.Right(data)) + } else { + emptyFlow() + } + } + .flowOn(dispatchers.io) } diff --git a/data/profile/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/implementation/ProfileStore.kt b/data/profile/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/implementation/ProfileStore.kt index 7e5948027..268965474 100644 --- a/data/profile/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/implementation/ProfileStore.kt +++ b/data/profile/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profile/implementation/ProfileStore.kt @@ -1,12 +1,12 @@ package com.thomaskioko.tvmaniac.profile.implementation import com.thomaskioko.tvmaniac.core.db.User -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse import com.thomaskioko.tvmaniac.profile.api.ProfileDao import com.thomaskioko.tvmaniac.resourcemanager.api.LastRequest import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.trakt.api.TraktUserRemoteDataSource import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.model.ApiResponse import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.Fetcher diff --git a/data/profile/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/profile/testing/FakeProfileRepository.kt b/data/profile/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/profile/testing/FakeProfileRepository.kt index d27162001..b98a6e139 100644 --- a/data/profile/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/profile/testing/FakeProfileRepository.kt +++ b/data/profile/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/profile/testing/FakeProfileRepository.kt @@ -2,20 +2,21 @@ package com.thomaskioko.tvmaniac.trakt.profile.testing import com.thomaskioko.tvmaniac.core.db.User import com.thomaskioko.tvmaniac.profile.api.ProfileRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow -import org.mobilenativefoundation.store.store5.StoreReadResponse class FakeProfileRepository : ProfileRepository { - private val userFlow: Channel> = Channel(Channel.UNLIMITED) + private val userFlow: Channel> = Channel(Channel.UNLIMITED) - suspend fun setUserData(response: StoreReadResponse) { + suspend fun setUserData(response: Either) { userFlow.send(response) } - override fun observeProfile(slug: String): Flow> = + override fun observeProfile(slug: String): Flow> = userFlow.receiveAsFlow() override suspend fun clearProfile() { diff --git a/data/profilestats/api/build.gradle.kts b/data/profilestats/api/build.gradle.kts index 978bdc459..30e94f05c 100644 --- a/data/profilestats/api/build.gradle.kts +++ b/data/profilestats/api/build.gradle.kts @@ -7,12 +7,9 @@ kotlin { commonMain { dependencies { api(projects.core.database) - api(projects.core.networkutil) api(projects.core.util) api(libs.coroutines.core) - api(libs.kotlinx.atomicfu) - api(libs.store5) } } } diff --git a/data/profilestats/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/api/StatsRepository.kt b/data/profilestats/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/api/StatsRepository.kt index 1f2cb32a2..f351705e7 100644 --- a/data/profilestats/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/api/StatsRepository.kt +++ b/data/profilestats/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/api/StatsRepository.kt @@ -1,9 +1,10 @@ package com.thomaskioko.tvmaniac.profilestats.api import com.thomaskioko.tvmaniac.core.db.Stats +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow -import org.mobilenativefoundation.store.store5.StoreReadResponse interface StatsRepository { - fun observeStats(slug: String): Flow> + fun observeStats(slug: String): Flow> } diff --git a/data/profilestats/implementation/build.gradle.kts b/data/profilestats/implementation/build.gradle.kts index 1f6a9d195..120f5b655 100644 --- a/data/profilestats/implementation/build.gradle.kts +++ b/data/profilestats/implementation/build.gradle.kts @@ -14,6 +14,8 @@ kotlin { implementation(libs.kotlinInject.runtime) implementation(libs.sqldelight.extensions) + implementation(libs.kotlinx.atomicfu) + implementation(libs.store5) } } } diff --git a/data/profilestats/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/implementation/StatsRepositoryImpl.kt b/data/profilestats/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/implementation/StatsRepositoryImpl.kt index e67679454..c802465c2 100644 --- a/data/profilestats/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/implementation/StatsRepositoryImpl.kt +++ b/data/profilestats/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/implementation/StatsRepositoryImpl.kt @@ -4,13 +4,20 @@ import com.thomaskioko.tvmaniac.core.db.Stats import com.thomaskioko.tvmaniac.profilestats.api.StatsRepository import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.StoreReadRequest -import org.mobilenativefoundation.store.store5.StoreReadResponse import kotlin.time.Duration.Companion.days +@OptIn(ExperimentalCoroutinesApi::class) @Inject class StatsRepositoryImpl( private val store: StatsStore, @@ -18,7 +25,7 @@ class StatsRepositoryImpl( private val dispatchers: AppCoroutineDispatchers, ) : StatsRepository { - override fun observeStats(slug: String): Flow> = + override fun observeStats(slug: String): Flow> = store.stream( StoreReadRequest.cached( key = slug, @@ -29,5 +36,14 @@ class StatsRepositoryImpl( ), ), ) + .distinctUntilChanged() + .flatMapLatest { + val data = it.dataOrNull() + if (data != null) { + flowOf(Either.Right(data)) + } else { + emptyFlow() + } + } .flowOn(dispatchers.io) } diff --git a/data/profilestats/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/implementation/StatsStore.kt b/data/profilestats/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/implementation/StatsStore.kt index 59aeb69ae..4df803e1f 100644 --- a/data/profilestats/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/implementation/StatsStore.kt +++ b/data/profilestats/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/profilestats/implementation/StatsStore.kt @@ -1,12 +1,12 @@ package com.thomaskioko.tvmaniac.profilestats.implementation import com.thomaskioko.tvmaniac.core.db.Stats -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse import com.thomaskioko.tvmaniac.profilestats.api.StatsDao import com.thomaskioko.tvmaniac.resourcemanager.api.LastRequest import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.trakt.api.TraktStatsRemoteDataSource import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.model.ApiResponse import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.Fetcher @@ -22,7 +22,7 @@ class StatsStore( private val mapper: StatsMapper, private val logger: KermitLogger, private val scope: AppCoroutineScope, -) : Store by storeBuilderFromFetcherAndSourceOfTruth( +) : Store by storeBuilderFromFetcherAndSourceOfTruth( fetcher = Fetcher.of { slug -> when (val response = remoteDataSource.getStats(slug)) { diff --git a/data/profilestats/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/profile/testing/FakeStatsRepository.kt b/data/profilestats/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/profile/testing/FakeStatsRepository.kt index 4b03efbfb..c1e9404f3 100644 --- a/data/profilestats/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/profile/testing/FakeStatsRepository.kt +++ b/data/profilestats/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/profile/testing/FakeStatsRepository.kt @@ -2,11 +2,12 @@ package com.thomaskioko.tvmaniac.trakt.profile.testing import com.thomaskioko.tvmaniac.core.db.Stats import com.thomaskioko.tvmaniac.profilestats.api.StatsRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -import org.mobilenativefoundation.store.store5.StoreReadResponse class FakeStatsRepository : StatsRepository { - override fun observeStats(slug: String): Flow> = + override fun observeStats(slug: String): Flow> = flowOf() } diff --git a/data/seasondetails/api/build.gradle.kts b/data/seasondetails/api/build.gradle.kts index 3702698fc..30e94f05c 100644 --- a/data/seasondetails/api/build.gradle.kts +++ b/data/seasondetails/api/build.gradle.kts @@ -7,11 +7,9 @@ kotlin { commonMain { dependencies { api(projects.core.database) - api(projects.core.networkutil) + api(projects.core.util) api(libs.coroutines.core) - api(libs.kotlinx.atomicfu) - api(libs.store5) } } } diff --git a/data/seasondetails/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/api/SeasonDetailsDao.kt b/data/seasondetails/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/api/SeasonDetailsDao.kt deleted file mode 100644 index 886107d9d..000000000 --- a/data/seasondetails/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/api/SeasonDetailsDao.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.thomaskioko.tvmaniac.seasondetails.api - -import com.thomaskioko.tvmaniac.core.db.SeasonWithEpisodes -import com.thomaskioko.tvmaniac.core.db.Season_episodes -import kotlinx.coroutines.flow.Flow - -interface SeasonDetailsDao { - - fun insert(entity: Season_episodes) - - fun observeShowEpisodes(showId: Long): Flow> - - fun delete(id: Long) - - fun deleteAll() -} diff --git a/data/seasondetails/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/api/SeasonDetailsRepository.kt b/data/seasondetails/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/api/SeasonDetailsRepository.kt index 2d8e9b530..be291958e 100644 --- a/data/seasondetails/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/api/SeasonDetailsRepository.kt +++ b/data/seasondetails/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/api/SeasonDetailsRepository.kt @@ -1,13 +1,13 @@ package com.thomaskioko.tvmaniac.seasondetails.api -import com.thomaskioko.tvmaniac.core.db.SeasonWithEpisodes -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure +import com.thomaskioko.tvmaniac.core.db.SeasonEpisodeDetailsById +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow interface SeasonDetailsRepository { - fun observeSeasonDetailsStream(traktId: Long): Flow>> + suspend fun fetchSeasonDetails(traktId: Long): List - fun observeSeasonDetails(traktId: Long): Flow>> + fun observeSeasonDetailsStream(traktId: Long): Flow>> } diff --git a/data/seasondetails/implementation/build.gradle.kts b/data/seasondetails/implementation/build.gradle.kts index 742fe3df8..af1dc42e1 100644 --- a/data/seasondetails/implementation/build.gradle.kts +++ b/data/seasondetails/implementation/build.gradle.kts @@ -13,12 +13,14 @@ kotlin { implementation(projects.core.traktApi.api) implementation(projects.core.util) implementation(projects.data.episodes.api) - implementation(projects.data.seasons.api) + implementation(projects.data.requestManager.api) implementation(projects.data.seasondetails.api) + implementation(projects.data.seasons.api) implementation(libs.kotlinInject.runtime) + implementation(libs.kotlinx.atomicfu) implementation(libs.sqldelight.extensions) - + implementation(libs.store5) } } diff --git a/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/Mapper.kt b/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/Mapper.kt index 33d7aa01f..9595a6c9b 100644 --- a/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/Mapper.kt +++ b/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/Mapper.kt @@ -1,31 +1,27 @@ package com.thomaskioko.tvmaniac.seasondetails.implementation -import com.thomaskioko.tvmaniac.core.db.Episodes -import com.thomaskioko.tvmaniac.core.db.SeasonWithEpisodes -import com.thomaskioko.tvmaniac.trakt.api.model.TraktEpisodesResponse +import com.thomaskioko.tvmaniac.core.db.Episode +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.trakt.api.model.TraktSeasonEpisodesResponse -fun TraktSeasonEpisodesResponse.toEpisodeCacheList(): List { - return episodes.map { episodeResponse -> - Episodes( - season_id = ids.trakt.toLong(), - trakt_id = episodeResponse.ids.trakt.toLong(), - tmdb_id = episodeResponse.ids.tmdb?.toLong(), - title = episodeResponse.title, - overview = episodeResponse.overview ?: "TBA", - ratings = episodeResponse.ratings, - runtime = episodeResponse.runtime.toLong(), - votes = episodeResponse.votes.toLong(), - episode_number = episodeResponse.episodeNumber.toString().padStart(2, '0'), +internal fun List.toSeasonWithEpisodes(): List { + return map { season -> + SeasonData( + seasonId = season.ids.trakt.toLong(), + title = season.title, + overview = season.overview ?: "TBA", + episodeCount = season.episodeCount.toLong(), + seasonNumber = season.number.toLong(), + episodes = season.toEpisodeCacheList(), ) } } -fun List.toEpisodeCache(seasonId: Long): List { - return map { episodeResponse -> - Episodes( - season_id = seasonId, - trakt_id = episodeResponse.ids.trakt.toLong(), +fun TraktSeasonEpisodesResponse.toEpisodeCacheList(): List { + return episodes.map { episodeResponse -> + Episode( + id = Id(episodeResponse.ids.trakt.toLong()), + season_id = Id(ids.trakt.toLong()), tmdb_id = episodeResponse.ids.tmdb?.toLong(), title = episodeResponse.title, overview = episodeResponse.overview ?: "TBA", @@ -37,7 +33,11 @@ fun List.toEpisodeCache(seasonId: Long): List { } } -fun List.toSeasonWithEpisodes(): List { - // TODO:: Add mapping #59 - return emptyList() -} +internal data class SeasonData( + val seasonId: Long, + val title: String, + val overview: String, + val seasonNumber: Long, + val episodeCount: Long, + val episodes: List, +) diff --git a/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsComponent.kt b/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsComponent.kt index 491235e1b..d5568bbbf 100644 --- a/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsComponent.kt +++ b/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsComponent.kt @@ -1,6 +1,5 @@ package com.thomaskioko.tvmaniac.seasondetails.implementation -import com.thomaskioko.tvmaniac.seasondetails.api.SeasonDetailsDao import com.thomaskioko.tvmaniac.seasondetails.api.SeasonDetailsRepository import com.thomaskioko.tvmaniac.util.scope.ApplicationScope import me.tatarka.inject.annotations.Provides @@ -12,8 +11,4 @@ interface SeasonDetailsComponent { fun provideSeasonDetailsRepository( bind: SeasonDetailsRepositoryImpl, ): SeasonDetailsRepository = bind - - @ApplicationScope - @Provides - fun provideSeasonsDetailsDao(bind: SeasonDetailsDaoImpl): SeasonDetailsDao = bind } diff --git a/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsDaoImpl.kt b/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsDaoImpl.kt deleted file mode 100644 index ea10d5227..000000000 --- a/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsDaoImpl.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.thomaskioko.tvmaniac.seasondetails.implementation - -import app.cash.sqldelight.coroutines.asFlow -import app.cash.sqldelight.coroutines.mapToList -import com.thomaskioko.tvmaniac.core.db.SeasonWithEpisodes -import com.thomaskioko.tvmaniac.core.db.Season_episodes -import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase -import com.thomaskioko.tvmaniac.seasondetails.api.SeasonDetailsDao -import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers -import kotlinx.coroutines.flow.Flow -import me.tatarka.inject.annotations.Inject - -@Inject -class SeasonDetailsDaoImpl( - private val database: TvManiacDatabase, - private val dispatcher: AppCoroutineDispatchers, -) : SeasonDetailsDao { - - override fun insert(entity: Season_episodes) { - database.transaction { - database.seasonEpisodesQueries.insertOrReplace( - show_id = entity.show_id, - season_id = entity.season_id, - season_number = entity.season_number, - ) - } - } - - override fun observeShowEpisodes(showId: Long): Flow> = - database.seasonEpisodesQueries.seasonWithEpisodes(showId) - .asFlow() - .mapToList(dispatcher.io) - - override fun delete(id: Long) { - database.seasonEpisodesQueries.delete(id) - } - - override fun deleteAll() { - database.transaction { - database.seasonEpisodesQueries.deleteAll() - } - } -} diff --git a/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsRepositoryImpl.kt b/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsRepositoryImpl.kt index 5d3e3bec8..ff4590c11 100644 --- a/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsRepositoryImpl.kt +++ b/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsRepositoryImpl.kt @@ -1,84 +1,55 @@ package com.thomaskioko.tvmaniac.seasondetails.implementation -import com.thomaskioko.tvmaniac.core.db.SeasonWithEpisodes -import com.thomaskioko.tvmaniac.core.db.Season_episodes -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse -import com.thomaskioko.tvmaniac.core.networkutil.DefaultError -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure -import com.thomaskioko.tvmaniac.core.networkutil.NetworkExceptionHandler -import com.thomaskioko.tvmaniac.core.networkutil.networkBoundResult -import com.thomaskioko.tvmaniac.episodes.api.EpisodesDao -import com.thomaskioko.tvmaniac.seasondetails.api.SeasonDetailsDao +import com.thomaskioko.tvmaniac.core.db.SeasonEpisodeDetailsById +import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.seasondetails.api.SeasonDetailsRepository -import com.thomaskioko.tvmaniac.trakt.api.TraktShowsRemoteDataSource -import com.thomaskioko.tvmaniac.trakt.api.model.ErrorResponse -import com.thomaskioko.tvmaniac.trakt.api.model.TraktSeasonEpisodesResponse -import com.thomaskioko.tvmaniac.util.KermitLogger import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject +import org.mobilenativefoundation.store.store5.StoreReadRequest +import org.mobilenativefoundation.store.store5.impl.extensions.get +import kotlin.time.Duration.Companion.days +@OptIn(ExperimentalCoroutinesApi::class) @Inject class SeasonDetailsRepositoryImpl( - private val remoteDataSource: TraktShowsRemoteDataSource, - private val seasonCache: SeasonDetailsDao, - private val episodesDao: EpisodesDao, - private val exceptionHandler: NetworkExceptionHandler, + private val seasonDetailsStore: SeasonDetailsStore, + private val requestManagerRepository: RequestManagerRepository, private val dispatcher: AppCoroutineDispatchers, - private val logger: KermitLogger, ) : SeasonDetailsRepository { - override fun observeSeasonDetailsStream(traktId: Long): Flow>> = - networkBoundResult( - query = { seasonCache.observeShowEpisodes(traktId) }, - shouldFetch = { it.isNullOrEmpty() }, - fetch = { remoteDataSource.getSeasonEpisodes(traktId) }, - saveFetchResult = { mapResponse(traktId, it) }, - exceptionHandler = exceptionHandler, - coroutineDispatcher = dispatcher.io, + override fun observeSeasonDetailsStream( + traktId: Long, + ): Flow>> = + seasonDetailsStore.stream( + StoreReadRequest.cached( + key = traktId, + refresh = requestManagerRepository.isRequestExpired( + entityId = traktId, + requestType = "SEASON_DETAILS", + threshold = 3.days, + ), + ), ) - - override fun observeSeasonDetails(traktId: Long): Flow>> = - seasonCache.observeShowEpisodes(traktId) - .catch { Either.Left(DefaultError(exceptionHandler.resolveError(it))) } - .map { Either.Right(it) } - - private fun mapResponse( - showId: Long, - response: ApiResponse, ErrorResponse>, - ) { - when (response) { - is ApiResponse.Success -> { - response.body.forEach { season -> - episodesDao.insert(season.toEpisodeCacheList()) - - seasonCache.insert( - Season_episodes( - show_id = showId, - season_id = season.ids.trakt.toLong(), - season_number = season.number.toLong(), - ), - ) + .distinctUntilChanged() + .flatMapLatest { + val data = it.dataOrNull() + if (data != null) { + flowOf(Either.Right(data)) + } else { + emptyFlow() } } + .flowOn(dispatcher.io) - is ApiResponse.Error.GenericError -> { - logger.error("observeSeasonDetails", "$response") - throw Throwable("${response.errorMessage}") - } - - is ApiResponse.Error.HttpError -> { - logger.error("observeSeasonDetails", "$response") - throw Throwable("${response.code} - ${response.errorMessage}") - } - - is ApiResponse.Error.SerializationError -> { - logger.error("observeSeasonDetails", "${response.errorMessage}") - throw Throwable("${response.errorMessage}") - } - } - } + override suspend fun fetchSeasonDetails(traktId: Long): List = + seasonDetailsStore.get(traktId) } diff --git a/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsStore.kt b/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsStore.kt index cfdff1f1e..d8f961290 100644 --- a/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsStore.kt +++ b/data/seasondetails/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/implementation/SeasonDetailsStore.kt @@ -1,13 +1,14 @@ package com.thomaskioko.tvmaniac.seasondetails.implementation -import com.thomaskioko.tvmaniac.core.db.Episodes -import com.thomaskioko.tvmaniac.core.db.SeasonWithEpisodes -import com.thomaskioko.tvmaniac.core.db.Season_episodes -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse +import com.thomaskioko.tvmaniac.core.db.Season +import com.thomaskioko.tvmaniac.core.db.SeasonEpisodeDetailsById +import com.thomaskioko.tvmaniac.db.DbTransactionRunner +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.episodes.api.EpisodesDao -import com.thomaskioko.tvmaniac.seasondetails.api.SeasonDetailsDao +import com.thomaskioko.tvmaniac.seasons.api.SeasonsDao import com.thomaskioko.tvmaniac.trakt.api.TraktShowsRemoteDataSource import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.model.ApiResponse import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.Fetcher @@ -18,12 +19,13 @@ import org.mobilenativefoundation.store.store5.StoreBuilder @Inject class SeasonDetailsStore( private val remoteDataSource: TraktShowsRemoteDataSource, - private val seasonDetailsDao: SeasonDetailsDao, + private val seasonCache: SeasonsDao, private val episodesDao: EpisodesDao, private val scope: AppCoroutineScope, + private val dbTransactionRunner: DbTransactionRunner, private val logger: KermitLogger, -) : Store> by StoreBuilder - .from, List>( +) : Store> by StoreBuilder + .from( fetcher = Fetcher.of { id: Long -> when (val response = remoteDataSource.getSeasonEpisodes(id)) { is ApiResponse.Success -> response.body.toSeasonWithEpisodes() @@ -44,34 +46,27 @@ class SeasonDetailsStore( } }, sourceOfTruth = SourceOfTruth.of( - reader = seasonDetailsDao::observeShowEpisodes, + reader = seasonCache::observeSeasonEpisodeDetailsById, writer = { id, list -> - list.forEach { season -> - episodesDao.insert( - Episodes( - trakt_id = season.trakt_id, - season_id = season.season_id, - title = season.title, - tmdb_id = season.tmdb_id, - overview = season.overview, - ratings = season.ratings, - runtime = season.runtime, - votes = season.votes, - episode_number = season.episode_number, - ), - ) + dbTransactionRunner { + list.forEach { season -> + seasonCache.upsert( + Season( + id = Id(season.seasonId), + show_id = Id(id), + season_number = season.seasonNumber, + title = season.title, + episode_count = season.episodeCount, + overview = season.overview, + ), + ) - seasonDetailsDao.insert( - Season_episodes( - show_id = id, - season_id = season.trakt_id, - season_number = season.season_number, - ), - ) + episodesDao.insert(season.episodes) + } } }, - delete = seasonDetailsDao::delete, - deleteAll = seasonDetailsDao::deleteAll, + delete = seasonCache::delete, + deleteAll = seasonCache::deleteAll, ), ) .scope(scope.io) diff --git a/data/seasondetails/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/testing/FakeSeasonDetailsRepository.kt b/data/seasondetails/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/testing/FakeSeasonDetailsRepository.kt index c2f7a12fd..eba976381 100644 --- a/data/seasondetails/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/testing/FakeSeasonDetailsRepository.kt +++ b/data/seasondetails/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/testing/FakeSeasonDetailsRepository.kt @@ -1,33 +1,31 @@ package com.thomaskioko.tvmaniac.seasondetails.testing -import com.thomaskioko.tvmaniac.core.db.SeasonWithEpisodes -import com.thomaskioko.tvmaniac.core.db.Seasons -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure +import com.thomaskioko.tvmaniac.core.db.SeasonEpisodeDetailsById import com.thomaskioko.tvmaniac.seasondetails.api.SeasonDetailsRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import org.mobilenativefoundation.store.store5.StoreReadResponse +import kotlinx.coroutines.flow.receiveAsFlow class FakeSeasonDetailsRepository : SeasonDetailsRepository { - private var seasonsResult = flowOf>>() + private val seasonsResult: Channel>> = Channel(Channel.UNLIMITED) + private val cachedResult: Channel> = Channel(Channel.UNLIMITED) - private var seasonEpisodesResult = flowOf>>() - - suspend fun setSeasonsResult(result: StoreReadResponse>) { - seasonsResult = flow { emit(result) } + suspend fun setSeasonsResult(result: Either>) { + seasonsResult.send(result) } - suspend fun setSeasonDetails(result: Either>) { - seasonEpisodesResult = flow { emit(result) } + suspend fun setCachedResults(result: List) { + cachedResult.send(result) } - override fun observeSeasonDetails(traktId: Long): Flow>> = - seasonEpisodesResult + override suspend fun fetchSeasonDetails( + traktId: Long, + ): List = cachedResult.receive() override fun observeSeasonDetailsStream( traktId: Long, - ): Flow>> = seasonEpisodesResult + ): Flow>> = seasonsResult.receiveAsFlow() } diff --git a/data/seasondetails/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/testing/MockData.kt b/data/seasondetails/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/testing/MockData.kt index f83ccbae7..a4b5c86a7 100644 --- a/data/seasondetails/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/testing/MockData.kt +++ b/data/seasondetails/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasondetails/testing/MockData.kt @@ -1,24 +1,25 @@ package com.thomaskioko.tvmaniac.seasondetails.testing -import com.thomaskioko.tvmaniac.core.db.SeasonWithEpisodes +import com.thomaskioko.tvmaniac.core.db.SeasonEpisodeDetailsById +import com.thomaskioko.tvmaniac.db.Id -val seasonDetails = listOf( - SeasonWithEpisodes( - trakt_id = 84958, - tmdb_id = 849583, - title = "Loki", +val SeasonWithEpisodeList = listOf( + SeasonEpisodeDetailsById( + show_id = Id(84958), + season_id = Id(12343), + show_title = "Loki", overview = "After stealing the Tesseract in Avengers: Endgame, Loki lands before the Time Variance Authority.", runtime = 45, - id = 12345, - season_id = 12343, season_number = 0, - episode_count = 15, - name = "Season 01", - trakt_id_ = 12345, - title_ = "Some title", + episode_count = 1, + season_title = "Season 01", + season_overview = "The journey to reunite the Ingham family continues as they travel to the USA.", ratings = 4.5, episode_number = "01", votes = 4958, - image_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", + episode_image_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", + episode_title = "Some title", + episode_season_id = Id(12345), + episode_id = Id(12345), ), ) diff --git a/data/seasons/api/build.gradle.kts b/data/seasons/api/build.gradle.kts index 3702698fc..30e94f05c 100644 --- a/data/seasons/api/build.gradle.kts +++ b/data/seasons/api/build.gradle.kts @@ -7,11 +7,9 @@ kotlin { commonMain { dependencies { api(projects.core.database) - api(projects.core.networkutil) + api(projects.core.util) api(libs.coroutines.core) - api(libs.kotlinx.atomicfu) - api(libs.store5) } } } diff --git a/data/seasons/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/api/SeasonsDao.kt b/data/seasons/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/api/SeasonsDao.kt index b66e72782..58d129411 100644 --- a/data/seasons/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/api/SeasonsDao.kt +++ b/data/seasons/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/api/SeasonsDao.kt @@ -1,15 +1,21 @@ package com.thomaskioko.tvmaniac.seasons.api -import com.thomaskioko.tvmaniac.core.db.Seasons +import com.thomaskioko.tvmaniac.core.db.Season +import com.thomaskioko.tvmaniac.core.db.SeasonEpisodeDetailsById +import com.thomaskioko.tvmaniac.core.db.SeasonsByShowId import kotlinx.coroutines.flow.Flow interface SeasonsDao { - fun insertSeason(season: Seasons) + fun upsert(season: Season) - fun insertSeasons(entityList: List) + fun upsert(entityList: List) - fun observeSeasons(traktId: Long): Flow> + fun fetchSeasonDetails(traktId: Long): List + + fun observeSeasonsByShowId(traktId: Long): Flow> + + fun observeSeasonEpisodeDetailsById(showId: Long): Flow> fun delete(id: Long) diff --git a/data/seasons/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/api/SeasonsRepository.kt b/data/seasons/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/api/SeasonsRepository.kt index ebeb06def..4224e76b1 100644 --- a/data/seasons/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/api/SeasonsRepository.kt +++ b/data/seasons/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/api/SeasonsRepository.kt @@ -1,11 +1,12 @@ package com.thomaskioko.tvmaniac.seasons.api -import com.thomaskioko.tvmaniac.core.db.Seasons +import com.thomaskioko.tvmaniac.core.db.SeasonsByShowId +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow -import org.mobilenativefoundation.store.store5.StoreReadResponse interface SeasonsRepository { - suspend fun getSeasons(traktId: Long): List - fun observeSeasonsStoreResponse(traktId: Long): Flow>> + suspend fun fetchSeasonsByShowId(traktId: Long): List + fun observeSeasonsByShowId(traktId: Long): Flow>> } diff --git a/data/seasons/implementation/build.gradle.kts b/data/seasons/implementation/build.gradle.kts index 54722660a..27f02d9ec 100644 --- a/data/seasons/implementation/build.gradle.kts +++ b/data/seasons/implementation/build.gradle.kts @@ -19,7 +19,8 @@ kotlin { implementation(libs.kotlinInject.runtime) implementation(libs.sqldelight.extensions) - + implementation(libs.kotlinx.atomicfu) + implementation(libs.store5) } } diff --git a/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/Mapper.kt b/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/Mapper.kt index 533d6d27d..21500488a 100644 --- a/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/Mapper.kt +++ b/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/Mapper.kt @@ -1,15 +1,16 @@ package com.thomaskioko.tvmaniac.seasons.implementation -import com.thomaskioko.tvmaniac.core.db.Seasons +import com.thomaskioko.tvmaniac.core.db.Season +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.trakt.api.model.TraktSeasonsResponse -fun List.toSeasonCacheList(traktId: Long): List = +fun List.toSeasonCacheList(traktId: Long): List = map { seasonResponse -> - Seasons( - show_trakt_id = traktId, - id = seasonResponse.ids.trakt.toLong(), + Season( + show_id = Id(traktId), + id = Id(id = seasonResponse.ids.trakt.toLong()), season_number = seasonResponse.number.toLong(), - name = seasonResponse.title, + title = seasonResponse.title, overview = seasonResponse.overview, episode_count = seasonResponse.episodeCount.toLong(), ) diff --git a/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsDaoImpl.kt b/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsDaoImpl.kt index 1e596acd3..0b344db0b 100644 --- a/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsDaoImpl.kt +++ b/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsDaoImpl.kt @@ -2,8 +2,11 @@ package com.thomaskioko.tvmaniac.seasons.implementation import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList -import com.thomaskioko.tvmaniac.core.db.Seasons +import com.thomaskioko.tvmaniac.core.db.Season +import com.thomaskioko.tvmaniac.core.db.SeasonEpisodeDetailsById +import com.thomaskioko.tvmaniac.core.db.SeasonsByShowId import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.seasons.api.SeasonsDao import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers import kotlinx.coroutines.flow.Flow @@ -17,31 +20,40 @@ class SeasonsDaoImpl( private val seasonQueries get() = database.seasonQueries - override fun insertSeason(season: Seasons) { + override fun upsert(season: Season) { database.transaction { seasonQueries.insertOrReplace( id = season.id, - show_trakt_id = season.show_trakt_id, + show_id = season.show_id, season_number = season.season_number, episode_count = season.episode_count, - name = season.name, + title = season.title, overview = season.overview, ) } } - override fun insertSeasons(entityList: List) { - entityList.forEach { insertSeason(it) } + override fun upsert(entityList: List) { + entityList.forEach { upsert(it) } } - override fun observeSeasons(traktId: Long): Flow> { - return seasonQueries.seasonById(traktId) + override fun observeSeasonsByShowId(traktId: Long): Flow> { + return database.seasonQueries.seasonsByShowId(Id(traktId)) .asFlow() .mapToList(dispatcher.io) } + override fun fetchSeasonDetails(traktId: Long): List = + database.seasonQueries.seasonEpisodeDetailsById(id = Id(traktId)) + .executeAsList() + + override fun observeSeasonEpisodeDetailsById(showId: Long): Flow> = + database.seasonQueries.seasonEpisodeDetailsById(id = Id(showId)) + .asFlow() + .mapToList(dispatcher.io) + override fun delete(id: Long) { - seasonQueries.delete(id) + seasonQueries.delete(Id(id)) } override fun deleteAll() { diff --git a/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsRepositoryImpl.kt b/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsRepositoryImpl.kt index 111e35373..3c28dcf5c 100644 --- a/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsRepositoryImpl.kt +++ b/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsRepositoryImpl.kt @@ -1,17 +1,24 @@ package com.thomaskioko.tvmaniac.seasons.implementation -import com.thomaskioko.tvmaniac.core.db.Seasons +import com.thomaskioko.tvmaniac.core.db.SeasonsByShowId import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.seasons.api.SeasonsRepository import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.StoreReadRequest -import org.mobilenativefoundation.store.store5.StoreReadResponse import org.mobilenativefoundation.store.store5.impl.extensions.get import kotlin.time.Duration.Companion.days +@OptIn(ExperimentalCoroutinesApi::class) @Inject class SeasonsRepositoryImpl( private val seasonsStore: SeasonsStore, @@ -19,10 +26,10 @@ class SeasonsRepositoryImpl( private val dispatcher: AppCoroutineDispatchers, ) : SeasonsRepository { - override suspend fun getSeasons(traktId: Long): List = + override suspend fun fetchSeasonsByShowId(traktId: Long): List = seasonsStore.get(traktId) - override fun observeSeasonsStoreResponse(traktId: Long): Flow>> = + override fun observeSeasonsByShowId(traktId: Long): Flow>> = seasonsStore.stream( StoreReadRequest.cached( key = traktId, @@ -33,5 +40,14 @@ class SeasonsRepositoryImpl( ), ), ) + .distinctUntilChanged() + .flatMapLatest { + val data = it.dataOrNull() + if (data != null) { + flowOf(Either.Right(data)) + } else { + emptyFlow() + } + } .flowOn(dispatcher.io) } diff --git a/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsStore.kt b/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsStore.kt index 54f07ca2e..afe4299a5 100644 --- a/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsStore.kt +++ b/data/seasons/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/implementation/SeasonsStore.kt @@ -1,12 +1,12 @@ package com.thomaskioko.tvmaniac.seasons.implementation -import com.thomaskioko.tvmaniac.core.db.Seasons -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse +import com.thomaskioko.tvmaniac.core.db.SeasonsByShowId import com.thomaskioko.tvmaniac.resourcemanager.api.LastRequest import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.seasons.api.SeasonsDao import com.thomaskioko.tvmaniac.trakt.api.TraktShowsRemoteDataSource import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.model.ApiResponse import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope import kotlinx.datetime.Clock import me.tatarka.inject.annotations.Inject @@ -22,7 +22,7 @@ class SeasonsStore( private val seasonsDao: SeasonsDao, private val scope: AppCoroutineScope, private val logger: KermitLogger, -) : Store> by StoreBuilder.from, List>( +) : Store> by StoreBuilder.from( fetcher = Fetcher.of { id -> when (val response = remoteDataSource.getShowSeasons(id)) { is ApiResponse.Success -> response.body.toSeasonCacheList(id) @@ -43,14 +43,14 @@ class SeasonsStore( } }, sourceOfTruth = SourceOfTruth.of( - reader = seasonsDao::observeSeasons, + reader = seasonsDao::observeSeasonsByShowId, writer = { id, list -> - seasonsDao.insertSeasons(list) + seasonsDao.upsert(list) requestManagerRepository.insert( LastRequest( - id = list.first().id, + id = list.first().id.id, entityId = id, requestType = "SEASON", timestamp = Clock.System.now(), diff --git a/data/seasons/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/testing/FakeSeasonsRepository.kt b/data/seasons/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/testing/FakeSeasonsRepository.kt index 413e0d877..eceb5705f 100644 --- a/data/seasons/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/testing/FakeSeasonsRepository.kt +++ b/data/seasons/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/testing/FakeSeasonsRepository.kt @@ -1,30 +1,36 @@ package com.thomaskioko.tvmaniac.seasons.testing -import com.thomaskioko.tvmaniac.core.db.SeasonWithEpisodes -import com.thomaskioko.tvmaniac.core.db.Seasons -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure +import com.thomaskioko.tvmaniac.core.db.SeasonsByShowId import com.thomaskioko.tvmaniac.seasons.api.SeasonsRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import org.mobilenativefoundation.store.store5.StoreReadResponse +import kotlinx.coroutines.flow.receiveAsFlow class FakeSeasonsRepository : SeasonsRepository { - private var seasonsResult = flowOf>>() + private var seasonsList: Channel> = Channel(Channel.UNLIMITED) + private var seasonsResult: Channel>> = Channel(Channel.UNLIMITED) - private var seasonEpisodesResult = flowOf>>() + private var seasonEpisodesResult: Channel>> = + Channel(Channel.UNLIMITED) - suspend fun setSeasonsResult(result: StoreReadResponse>) { - seasonsResult = flow { emit(result) } + suspend fun setSeasonWithEpisodes(result: Either>) { + seasonEpisodesResult.send(result) } - suspend fun setSeasonDetails(result: Either>) { - seasonEpisodesResult = flow { emit(result) } + suspend fun setSeasons(result: List) { + seasonsList.send(result) } - override suspend fun getSeasons(traktId: Long): List = emptyList() + suspend fun setSeasonsResult(result: Either>) { + seasonsResult.send(result) + } + + override suspend fun fetchSeasonsByShowId(traktId: Long): List = seasonsList.receive() - override fun observeSeasonsStoreResponse(traktId: Long): Flow>> = seasonsResult + override fun observeSeasonsByShowId( + traktId: Long, + ): Flow>> = seasonsResult.receiveAsFlow() } diff --git a/data/seasons/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/testing/MockData.kt b/data/seasons/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/testing/MockData.kt deleted file mode 100644 index cdf6a1b02..000000000 --- a/data/seasons/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/seasons/testing/MockData.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.thomaskioko.tvmaniac.seasons.testing - -import com.thomaskioko.tvmaniac.core.db.SeasonWithEpisodes - -val seasonDetails = listOf( - SeasonWithEpisodes( - trakt_id = 84958, - tmdb_id = 849583, - title = "Loki", - overview = "After stealing the Tesseract in Avengers: Endgame, Loki lands before the Time Variance Authority.", - runtime = 45, - id = 12345, - season_id = 12343, - season_number = 0, - episode_count = 15, - name = "Season 01", - trakt_id_ = 12345, - title_ = "Some title", - ratings = 4.5, - episode_number = "01", - votes = 4958, - image_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", - ), -) diff --git a/data/showimages/api/build.gradle.kts b/data/showimages/api/build.gradle.kts index 59e60b379..30e94f05c 100644 --- a/data/showimages/api/build.gradle.kts +++ b/data/showimages/api/build.gradle.kts @@ -7,7 +7,7 @@ kotlin { commonMain { dependencies { api(projects.core.database) - api(projects.core.networkutil) + api(projects.core.util) api(libs.coroutines.core) } diff --git a/data/showimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/api/ShowImagesDao.kt b/data/showimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/api/ShowImagesDao.kt index 1adf44f43..164483c7e 100644 --- a/data/showimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/api/ShowImagesDao.kt +++ b/data/showimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/api/ShowImagesDao.kt @@ -1,12 +1,20 @@ package com.thomaskioko.tvmaniac.showimages.api -import com.thomaskioko.tvmaniac.core.db.SelectShowImages +import com.thomaskioko.tvmaniac.core.db.EmptyShowImage import com.thomaskioko.tvmaniac.core.db.Show_image import kotlinx.coroutines.flow.Flow interface ShowImagesDao { - fun insert(image: Show_image) + fun upsert(images: List) - fun observeShowImages(): Flow> + fun upsert(image: Show_image) + + fun observeShowImages(): Flow> + + fun fetchShowImages(): List + + fun delete(id: Long) + + fun deleteAll() } diff --git a/data/showimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/api/ShowImagesRepository.kt b/data/showimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/api/ShowImagesRepository.kt index 49ea9df12..5cb3717c8 100644 --- a/data/showimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/api/ShowImagesRepository.kt +++ b/data/showimages/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/api/ShowImagesRepository.kt @@ -1,7 +1,7 @@ package com.thomaskioko.tvmaniac.showimages.api -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow interface ShowImagesRepository { diff --git a/data/showimages/implementation/build.gradle.kts b/data/showimages/implementation/build.gradle.kts index 11b76c2e6..1285c1f04 100644 --- a/data/showimages/implementation/build.gradle.kts +++ b/data/showimages/implementation/build.gradle.kts @@ -14,7 +14,9 @@ kotlin { implementation(projects.core.tmdbApi.api) implementation(libs.kotlinInject.runtime) + implementation(libs.kotlinx.atomicfu) implementation(libs.sqldelight.extensions) + implementation(libs.store5) } } diff --git a/data/showimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/implementation/ShowImagesDaoImpl.kt b/data/showimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/implementation/ShowImagesDaoImpl.kt index 4317e5410..2fa8a2759 100644 --- a/data/showimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/implementation/ShowImagesDaoImpl.kt +++ b/data/showimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/implementation/ShowImagesDaoImpl.kt @@ -2,9 +2,10 @@ package com.thomaskioko.tvmaniac.showimages.implementation import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList -import com.thomaskioko.tvmaniac.core.db.SelectShowImages +import com.thomaskioko.tvmaniac.core.db.EmptyShowImage import com.thomaskioko.tvmaniac.core.db.Show_image import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.showimages.api.ShowImagesDao import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers import kotlinx.coroutines.flow.Flow @@ -16,20 +17,37 @@ class ShowImagesDaoImpl( private val dispatchers: AppCoroutineDispatchers, ) : ShowImagesDao { - override fun insert(image: Show_image) { + override fun upsert(images: List) { database.transaction { - database.show_imageQueries.insertOrReplace( - trakt_id = image.trakt_id, - tmdb_id = image.tmdb_id, - poster_url = image.poster_url, - backdrop_url = image.backdrop_url, - ) + images.forEach { upsert(it) } } } - override fun observeShowImages(): Flow> { - return database.show_imageQueries.selectShowImages() + override fun upsert(image: Show_image) { + database.show_imageQueries.insertOrReplace( + id = image.id, + tmdb_id = image.tmdb_id, + poster_url = image.poster_url, + backdrop_url = image.backdrop_url, + ) + } + + override fun observeShowImages(): Flow> { + return database.show_imageQueries.emptyShowImage() .asFlow() .mapToList(dispatchers.io) } + + override fun fetchShowImages(): List { + return database.show_imageQueries.emptyShowImage() + .executeAsList() + } + + override fun delete(id: Long) { + database.show_imageQueries.delete(Id(id)) + } + + override fun deleteAll() { + database.show_imageQueries.deleteAll() + } } diff --git a/data/showimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/implementation/ShowImagesRepositoryImpl.kt b/data/showimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/implementation/ShowImagesRepositoryImpl.kt index 67ae3ef6d..52dc3432b 100644 --- a/data/showimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/implementation/ShowImagesRepositoryImpl.kt +++ b/data/showimages/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/showimages/implementation/ShowImagesRepositoryImpl.kt @@ -1,11 +1,7 @@ package com.thomaskioko.tvmaniac.showimages.implementation import com.thomaskioko.tvmaniac.core.db.Show_image -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse -import com.thomaskioko.tvmaniac.core.networkutil.DefaultError -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure -import com.thomaskioko.tvmaniac.core.networkutil.NetworkExceptionHandler +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.resourcemanager.api.LastRequest import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.showimages.api.ShowImagesDao @@ -13,13 +9,17 @@ import com.thomaskioko.tvmaniac.showimages.api.ShowImagesRepository import com.thomaskioko.tvmaniac.tmdb.api.TmdbNetworkDataSource import com.thomaskioko.tvmaniac.util.FormatterUtil import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.NetworkExceptionHandler +import com.thomaskioko.tvmaniac.util.model.ApiResponse import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.model.DefaultError +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import me.tatarka.inject.annotations.Inject -import kotlin.time.Duration.Companion.days @Inject class ShowImagesRepositoryImpl( @@ -36,41 +36,33 @@ class ShowImagesRepositoryImpl( imageCache.observeShowImages() .map { shows -> shows.forEach { show -> - val shouldFetch = requestManagerRepository.isRequestExpired( - entityId = show.trakt_id, - requestType = "SHOW_ARTWORK", - threshold = 1.days, - ) - if (shouldFetch) { - show.tmdb_id?.let { tmdbId -> - - when (val response = networkDataSource.getTvShowDetails(tmdbId)) { - is ApiResponse.Error -> { - logger.error("updateShowArtWork", "$response") - } + show.tmdb_id?.let { tmdbId -> + when (val response = networkDataSource.getTvShowDetails(tmdbId)) { + is ApiResponse.Error -> { + logger.error("updateShowArtWork", "$response") + } - is ApiResponse.Success -> { - imageCache.insert( - Show_image( - trakt_id = show.trakt_id, - tmdb_id = tmdbId, - poster_url = response.body.posterPath?.let { - formatterUtil.formatTmdbPosterPath(it) - }, - backdrop_url = response.body.backdropPath?.let { - formatterUtil.formatTmdbPosterPath(it) - }, - ), - ) + is ApiResponse.Success -> { + imageCache.upsert( + Show_image( + id = Id(id = show.id.id), + tmdb_id = tmdbId, + poster_url = response.body.posterPath?.let { + formatterUtil.formatTmdbPosterPath(it) + }, + backdrop_url = response.body.backdropPath?.let { + formatterUtil.formatTmdbPosterPath(it) + }, + ), + ) - requestManagerRepository.insert( - LastRequest( - id = tmdbId, - entityId = show.trakt_id, - requestType = "SHOW_ARTWORK", - ), - ) - } + requestManagerRepository.insert( + LastRequest( + id = tmdbId, + entityId = show.id.id, + requestType = "SHOW_ARTWORK", + ), + ) } } } diff --git a/data/showimages/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/testing/FakeShowImagesRepository.kt b/data/showimages/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/testing/FakeShowImagesRepository.kt index 10dde84fa..cb2746a1f 100644 --- a/data/showimages/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/testing/FakeShowImagesRepository.kt +++ b/data/showimages/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/testing/FakeShowImagesRepository.kt @@ -1,8 +1,8 @@ package com.thomaskioko.tvmaniac.tmdb.testing -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure import com.thomaskioko.tvmaniac.showimages.api.ShowImagesRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf diff --git a/data/shows/api/build.gradle.kts b/data/shows/api/build.gradle.kts index 1437f8489..ad9438ae9 100644 --- a/data/shows/api/build.gradle.kts +++ b/data/shows/api/build.gradle.kts @@ -7,12 +7,9 @@ kotlin { commonMain { dependencies { api(projects.core.database) - api(projects.core.networkutil) api(projects.data.category.api) api(libs.coroutines.core) - api(libs.kotlinx.atomicfu) - api(libs.store5) } } } diff --git a/data/shows/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/DiscoverRepository.kt b/data/shows/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/DiscoverRepository.kt index 928891f5d..ae538c16d 100644 --- a/data/shows/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/DiscoverRepository.kt +++ b/data/shows/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/DiscoverRepository.kt @@ -3,22 +3,20 @@ package com.thomaskioko.tvmaniac.shows.api import com.thomaskioko.tvmaniac.category.api.model.Category import com.thomaskioko.tvmaniac.core.db.ShowById import com.thomaskioko.tvmaniac.core.db.ShowsByCategory +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow -import org.mobilenativefoundation.store.store5.StoreReadResponse +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days interface DiscoverRepository { - fun observeShow(traktId: Long): Flow> + fun observeShow(traktId: Long): Flow> - fun observeShowsByCategory(categoryId: Long): Flow>> - - fun observeTrendingShows(): Flow>> - - fun observePopularShows(): Flow>> - - fun observeAnticipatedShows(): Flow>> - - fun observeRecommendedShows(): Flow>> + fun observeShowCategory( + category: Category, + duration: Duration = 3.days, + ): Flow>> suspend fun fetchDiscoverShows() diff --git a/data/shows/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/ShowsDao.kt b/data/shows/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/ShowsDao.kt index 4393a466b..c2720edf6 100644 --- a/data/shows/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/ShowsDao.kt +++ b/data/shows/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/ShowsDao.kt @@ -8,9 +8,9 @@ import kotlinx.coroutines.flow.Flow interface ShowsDao { - fun insert(show: Show) + fun upsert(show: Show) - fun insert(list: List) + fun upsert(list: List) fun observeTvShow(showId: Long): Flow @@ -21,6 +21,4 @@ interface ShowsDao { fun getTvShow(traktId: Long): ShowById fun deleteTvShows() - - fun getShowsByCategoryID(categoryId: Long): List } diff --git a/data/shows/implementation/build.gradle.kts b/data/shows/implementation/build.gradle.kts index fb0c55523..40aeabdf2 100644 --- a/data/shows/implementation/build.gradle.kts +++ b/data/shows/implementation/build.gradle.kts @@ -17,6 +17,8 @@ kotlin { implementation(libs.kotlinInject.runtime) implementation(libs.sqldelight.extensions) + implementation(libs.kotlinx.atomicfu) + implementation(libs.store5) } } diff --git a/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverRepositoryImpl.kt b/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverRepositoryImpl.kt index b28e4c47c..21e36e4e0 100644 --- a/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverRepositoryImpl.kt +++ b/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverRepositoryImpl.kt @@ -5,111 +5,67 @@ import com.thomaskioko.tvmaniac.category.api.model.Category.ANTICIPATED import com.thomaskioko.tvmaniac.category.api.model.Category.POPULAR import com.thomaskioko.tvmaniac.category.api.model.Category.RECOMMENDED import com.thomaskioko.tvmaniac.category.api.model.Category.TRENDING -import com.thomaskioko.tvmaniac.category.api.model.getCategory import com.thomaskioko.tvmaniac.core.db.ShowById import com.thomaskioko.tvmaniac.core.db.ShowsByCategory import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.shows.api.DiscoverRepository import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.StoreReadRequest import org.mobilenativefoundation.store.store5.StoreReadResponse import org.mobilenativefoundation.store.store5.impl.extensions.get +import kotlin.time.Duration import kotlin.time.Duration.Companion.days +@OptIn(ExperimentalCoroutinesApi::class) @Inject -class DiscoverRepositoryImpl constructor( +class DiscoverRepositoryImpl( private val showStore: ShowStore, private val discoverShowsStore: DiscoverShowsStore, private val requestManagerRepository: RequestManagerRepository, private val dispatchers: AppCoroutineDispatchers, ) : DiscoverRepository { - override suspend fun getShowById(traktId: Long): ShowById = - showStore.get(key = traktId) + override suspend fun getShowById(traktId: Long): ShowById = showStore.get(key = traktId) - override fun observeShow(traktId: Long): Flow> = - showStore.stream( - StoreReadRequest.cached( - key = traktId, - refresh = requestManagerRepository.isRequestExpired( - entityId = traktId, - requestType = "SHOW_DETAILS", - threshold = 6.days, - ), + override fun observeShow(traktId: Long): Flow> = showStore.stream( + StoreReadRequest.cached( + key = traktId, + refresh = requestManagerRepository.isRequestExpired( + entityId = traktId, + requestType = "SHOW_DETAILS", + threshold = 6.days, ), - ) - .flowOn(dispatchers.io) + ), + ) + .mapResult() override suspend fun fetchShows(category: Category): List = discoverShowsStore.get(key = category) - override fun observeShowsByCategory(categoryId: Long): Flow>> = - discoverShowsStore.stream( - StoreReadRequest.cached( - key = categoryId.getCategory(), - refresh = requestManagerRepository.isRequestExpired( - entityId = categoryId, - requestType = categoryId.getCategory().title, - threshold = 3.days, - ), - ), - ) - .flowOn(dispatchers.io) - - override fun observeTrendingShows(): Flow>> = - discoverShowsStore.stream( - StoreReadRequest.cached( - key = TRENDING, - refresh = requestManagerRepository.isRequestExpired( - entityId = TRENDING.id, - requestType = TRENDING.title, - threshold = 3.days, - ), - ), - ) - .flowOn(dispatchers.io) - - override fun observePopularShows(): Flow>> = - discoverShowsStore.stream( - StoreReadRequest.cached( - key = POPULAR, - refresh = requestManagerRepository.isRequestExpired( - entityId = POPULAR.id, - requestType = POPULAR.title, - threshold = 3.days, - ), - ), - ) - .flowOn(dispatchers.io) - - override fun observeAnticipatedShows(): Flow>> = - discoverShowsStore.stream( - StoreReadRequest.cached( - key = ANTICIPATED, - refresh = requestManagerRepository.isRequestExpired( - entityId = ANTICIPATED.id, - requestType = ANTICIPATED.title, - threshold = 3.days, - ), - ), - ) - .flowOn(dispatchers.io) - - override fun observeRecommendedShows(): Flow>> = - discoverShowsStore.stream( - StoreReadRequest.cached( - key = RECOMMENDED, - refresh = requestManagerRepository.isRequestExpired( - entityId = RECOMMENDED.id, - requestType = RECOMMENDED.title, - threshold = 1.days, - ), + override fun observeShowCategory( + category: Category, + duration: Duration, + ): Flow>> = discoverShowsStore.stream( + StoreReadRequest.cached( + key = category, + refresh = requestManagerRepository.isRequestExpired( + entityId = category.id, + requestType = category.title, + threshold = duration, ), - ) - .flowOn(dispatchers.io) + ), + ) + .mapResult() override suspend fun fetchDiscoverShows() { val categories = listOf(TRENDING, POPULAR, ANTICIPATED, RECOMMENDED) @@ -118,4 +74,16 @@ class DiscoverRepositoryImpl constructor( discoverShowsStore.get(category) } } + + private fun Flow>.mapResult(): Flow> = + distinctUntilChanged() + .flatMapLatest { + val data = it.dataOrNull() + if (data != null) { + flowOf(Either.Right(data)) + } else { + emptyFlow() + } + } + .flowOn(dispatchers.io) } diff --git a/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverResponseMapper.kt b/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverResponseMapper.kt index 43aa2a87e..8db1bdaa5 100644 --- a/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverResponseMapper.kt +++ b/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverResponseMapper.kt @@ -5,7 +5,7 @@ import com.thomaskioko.tvmaniac.core.db.Show import com.thomaskioko.tvmaniac.core.db.ShowById import com.thomaskioko.tvmaniac.core.db.Show_category import com.thomaskioko.tvmaniac.core.db.ShowsByCategory -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.resourcemanager.api.LastRequest import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.trakt.api.model.ErrorResponse @@ -13,6 +13,7 @@ import com.thomaskioko.tvmaniac.trakt.api.model.TraktShowResponse import com.thomaskioko.tvmaniac.trakt.api.model.TraktShowsResponse import com.thomaskioko.tvmaniac.util.FormatterUtil import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.model.ApiResponse import me.tatarka.inject.annotations.Inject @Inject @@ -35,10 +36,12 @@ class DiscoverResponseMapper( logger.error("ShowStore GenericError", "${response.errorBody}") throw Throwable("${response.errorMessage}") } + is ApiResponse.Error.GenericError -> { logger.error("ShowStore GenericError", "${response.errorMessage}") throw Throwable("${response.errorMessage}") } + is ApiResponse.Error.SerializationError -> { logger.error("ShowStore GenericError", "${response.errorMessage}") throw Throwable("${response.errorMessage}") @@ -58,10 +61,12 @@ class DiscoverResponseMapper( logger.error("ShowStore GenericError", "${response.errorBody}") throw Throwable("${response.errorMessage}") } + is ApiResponse.Error.SerializationError -> { logger.error("ShowStore GenericError", "${response.errorMessage}") throw Throwable("${response.errorMessage}") } + is ApiResponse.Error.GenericError -> { logger.error("ShowStore GenericError", "${response.errorMessage}") throw Throwable("${response.errorMessage}") @@ -69,7 +74,7 @@ class DiscoverResponseMapper( } private fun responseToEntity(response: TraktShowResponse) = ShowsByCategory( - trakt_id = response.ids.trakt.toLong(), + id = Id(id = response.ids.trakt.toLong()), tmdb_id = response.ids.tmdb?.toLong(), title = response.title, overview = response.overview ?: "", @@ -87,7 +92,7 @@ class DiscoverResponseMapper( ) fun responseToShow(response: TraktShowResponse) = ShowById( - trakt_id = response.ids.trakt.toLong(), + id = Id(id = response.ids.trakt.toLong()), tmdb_id = response.ids.tmdb?.toLong(), title = response.title, overview = response.overview ?: "", @@ -99,18 +104,14 @@ class DiscoverResponseMapper( rating = formatterUtil.formatDouble(response.rating, 1), genres = response.genres.map { it.replaceFirstChar { it.uppercase() } }, status = response.status.replaceFirstChar { it.uppercase() }, - trakt_id_ = null, - tmdb_id_ = null, poster_url = null, backdrop_url = null, - id = null, - created_at = null, - synced = false, + in_watchlist = 0L, ) fun toShow(showById: ShowById) = Show( - trakt_id = showById.trakt_id, + id = showById.id, tmdb_id = showById.tmdb_id, title = showById.title, overview = showById.overview, @@ -126,7 +127,7 @@ class DiscoverResponseMapper( private fun showResponseToCacheList(response: TraktShowsResponse): ShowsByCategory = ShowsByCategory( - trakt_id = response.show.ids.trakt.toLong(), + id = Id(id = response.show.ids.trakt.toLong()), tmdb_id = response.show.ids.tmdb?.toLong(), title = response.show.title, overview = response.show.overview ?: "", @@ -145,8 +146,8 @@ class DiscoverResponseMapper( fun toCategoryCache(shows: List, categoryId: Long) = shows.map { Show_category( - trakt_id = it.trakt_id, - category_id = categoryId, + id = Id(id = it.id.id), + category_id = Id(categoryId), ) } } diff --git a/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverShowsStore.kt b/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverShowsStore.kt index 4a95c4894..582847bb3 100644 --- a/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverShowsStore.kt +++ b/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/DiscoverShowsStore.kt @@ -47,7 +47,7 @@ class DiscoverShowsStore( val shows = list.map { Show( - trakt_id = it.trakt_id, + id = it.id, tmdb_id = it.tmdb_id, title = it.title, overview = it.overview, @@ -61,8 +61,8 @@ class DiscoverShowsStore( genres = it.genres, ) } - showsDao.insert(shows) - categoryCache.insert(mapper.toCategoryCache(shows, category.id)) + showsDao.upsert(shows) + categoryCache.upsert(mapper.toCategoryCache(shows, category.id)) }, ), ) diff --git a/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/ShowDaoImpl.kt b/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/ShowDaoImpl.kt index ce12d3ea4..5107b4362 100644 --- a/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/ShowDaoImpl.kt +++ b/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/ShowDaoImpl.kt @@ -8,21 +8,22 @@ import com.thomaskioko.tvmaniac.core.db.ShowById import com.thomaskioko.tvmaniac.core.db.Shows import com.thomaskioko.tvmaniac.core.db.ShowsByCategory import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.shows.api.ShowsDao import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers import kotlinx.coroutines.flow.Flow import me.tatarka.inject.annotations.Inject @Inject -class ShowDaoImpl constructor( +class ShowDaoImpl( private val database: TvManiacDatabase, private val dispatchers: AppCoroutineDispatchers, ) : ShowsDao { - override fun insert(show: Show) { + override fun upsert(show: Show) { database.showQueries.transaction { database.showQueries.insertOrReplace( - trakt_id = show.trakt_id, + id = show.id, tmdb_id = show.tmdb_id, title = show.title, overview = show.overview, @@ -37,18 +38,18 @@ class ShowDaoImpl constructor( } } - override fun insert(list: List) { - list.forEach { insert(it) } + override fun upsert(list: List) { + list.forEach { upsert(it) } } override fun observeTvShow(showId: Long): Flow { - return database.showQueries.showById(showId) + return database.showQueries.showById(Id(showId)) .asFlow() .mapToOne(dispatchers.io) } override fun observeCachedShows(categoryId: Long): Flow> { - return database.showQueries.showsByCategory(categoryId) + return database.show_categoryQueries.showsByCategory(Id(categoryId)) .asFlow() .mapToList(dispatchers.io) } @@ -60,14 +61,10 @@ class ShowDaoImpl constructor( } override fun getTvShow(traktId: Long): ShowById = - database.showQueries.showById(traktId) + database.showQueries.showById(Id(traktId)) .executeAsOne() override fun deleteTvShows() { database.showQueries.deleteAll() } - - override fun getShowsByCategoryID(categoryId: Long): List = - database.showQueries.showsByCategory(categoryId) - .executeAsList() } diff --git a/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/ShowStore.kt b/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/ShowStore.kt index 5e8e16573..710ce4799 100644 --- a/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/ShowStore.kt +++ b/data/shows/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/implementation/ShowStore.kt @@ -1,12 +1,12 @@ package com.thomaskioko.tvmaniac.shows.implementation import com.thomaskioko.tvmaniac.core.db.ShowById -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse import com.thomaskioko.tvmaniac.resourcemanager.api.LastRequest import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.shows.api.ShowsDao import com.thomaskioko.tvmaniac.trakt.api.TraktShowsRemoteDataSource import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.model.ApiResponse import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.Fetcher @@ -49,11 +49,11 @@ class ShowStore( reader = { traktId -> showsDao.observeTvShow(traktId) }, writer = { id, show -> - showsDao.insert(mapper.toShow(show)) + showsDao.upsert(mapper.toShow(show)) requestManagerRepository.insert( LastRequest( - id = id + show.trakt_id, + id = id + show.id.id, entityId = id, requestType = "SHOW_DETAILS", ), diff --git a/data/shows/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/testing/FakeDiscoverRepository.kt b/data/shows/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/testing/FakeDiscoverRepository.kt index dafc7c483..64e4d25f4 100644 --- a/data/shows/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/testing/FakeDiscoverRepository.kt +++ b/data/shows/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/testing/FakeDiscoverRepository.kt @@ -3,75 +3,60 @@ package com.thomaskioko.tvmaniac.shows.testing import com.thomaskioko.tvmaniac.category.api.model.Category import com.thomaskioko.tvmaniac.core.db.ShowById import com.thomaskioko.tvmaniac.core.db.ShowsByCategory +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.shows.api.DiscoverRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import org.mobilenativefoundation.store.store5.StoreReadResponse +import kotlinx.coroutines.flow.receiveAsFlow +import kotlin.time.Duration class FakeDiscoverRepository : DiscoverRepository { - private var featuredResult = flowOf>>() + private var showById: Channel = Channel(Channel.UNLIMITED) + private var updatedShowCategoryResult: Channel>> = + Channel(Channel.UNLIMITED) - private var anticipatedResult = flowOf>>() + private var showCategoryResult: Channel> = Channel(Channel.UNLIMITED) - private var popularResult = flowOf>>() + private var showByIdResult: Channel> = + Channel(Channel.UNLIMITED) - private var trendingResult = flowOf>>() - - private var showResult = flowOf>() - - suspend fun setFeaturedResult(result: StoreReadResponse>) { - featuredResult = flow { emit(result) } - } - - suspend fun setAnticipatedResult(result: StoreReadResponse>) { - anticipatedResult = flow { emit(result) } - } - - suspend fun setPopularResult(result: StoreReadResponse>) { - popularResult = flow { emit(result) } + suspend fun setShowCategory(result: List) { + showCategoryResult.send(result) } - suspend fun setTrendingResult(result: StoreReadResponse>) { - trendingResult = flow { emit(result) } + suspend fun setShowById(result: ShowById) { + showById.send(result) } - suspend fun setShowResult(result: StoreReadResponse) { - showResult = flow { emit(result) } + suspend fun setTrendingResult(result: Either>) { + updatedShowCategoryResult.send(result) } - override fun observeShow(traktId: Long): Flow> = showResult - - override fun observeShowsByCategory( - categoryId: Long, - ): Flow>> = featuredResult - - override fun observeTrendingShows(): Flow>> { - return trendingResult - } - - override fun observePopularShows(): Flow>> { - return popularResult + suspend fun setShowResult(result: Either) { + showByIdResult.send(result) } - override fun observeAnticipatedShows(): Flow>> { - return anticipatedResult - } + override fun observeShow(traktId: Long): Flow> = + showByIdResult.receiveAsFlow() - override fun observeRecommendedShows(): Flow>> { - return featuredResult - } + override fun observeShowCategory( + category: Category, + duration: Duration, + ): Flow>> = updatedShowCategoryResult.receiveAsFlow() override suspend fun fetchDiscoverShows() {} - override suspend fun fetchShows(category: Category): List = emptyList() + override suspend fun fetchShows(category: Category): List = + showCategoryResult.receive() - override suspend fun getShowById(traktId: Long): ShowById = selectedShow + override suspend fun getShowById(traktId: Long): ShowById = showById.receive() } val selectedShow = ShowById( - trakt_id = 84958, + id = Id(84958), tmdb_id = 849583, title = "Loki", overview = "After stealing the Tesseract during the events of “Avengers: Endgame,” " + @@ -90,9 +75,5 @@ val selectedShow = ShowById( poster_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", backdrop_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", aired_episodes = 12, - trakt_id_ = 1234, - id = 12345, - created_at = null, - synced = false, - tmdb_id_ = 1232, + in_watchlist = 0, ) diff --git a/data/shows/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/testing/MockData.kt b/data/shows/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/testing/MockData.kt deleted file mode 100644 index 93227f89a..000000000 --- a/data/shows/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/testing/MockData.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.thomaskioko.tvmaniac.shows.testing - -import com.thomaskioko.tvmaniac.core.db.SelectWatchlist - -val selectWatchList = listOf( - SelectWatchlist( - trakt_id = 84958, - tmdb_id = 849583, - title = "Loki", - overview = "After stealing the Tesseract during the events of “Avengers: Endgame,” " + - "an alternate version of Loki is brought to the mysterious Time Variance " + - "Authority, a bureaucratic organization that exists outside of time and " + - "space and monitors the timeline. They give Loki a choice: face being " + - "erased from existence due to being a “time variant”or help fix " + - "the timeline and stop a greater threat.", - language = "en", - votes = 4958, - rating = 8.1, - genres = listOf("Horror", "Action"), - status = "Returning Series", - year = "2024", - runtime = 45, - poster_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", - backdrop_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", - aired_episodes = 12, - id = 84958, - synced = true, - created_at = 12345645, - trakt_id_ = 1232, - tmdb_id_ = 849583, - ), -) diff --git a/data/similar/api/build.gradle.kts b/data/similar/api/build.gradle.kts index cc0798de1..28aa509b8 100644 --- a/data/similar/api/build.gradle.kts +++ b/data/similar/api/build.gradle.kts @@ -8,13 +8,10 @@ kotlin { commonMain { dependencies { - api(projects.core.networkutil) api(projects.core.database) implementation(projects.data.shows.api) api(libs.coroutines.core) - api(libs.kotlinx.atomicfu) - api(libs.store5) } } } diff --git a/data/similar/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/api/SimilarShowsDao.kt b/data/similar/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/api/SimilarShowsDao.kt index e5a288a6b..fb0c8acf0 100644 --- a/data/similar/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/api/SimilarShowsDao.kt +++ b/data/similar/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/api/SimilarShowsDao.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow interface SimilarShowsDao { - fun insert(traktId: Long, similarShowId: Long) + fun upsert(showId: Long, similarShowId: Long) fun observeSimilarShows(traktId: Long): Flow> diff --git a/data/similar/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/api/SimilarShowsRepository.kt b/data/similar/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/api/SimilarShowsRepository.kt index 111ff57c7..f5f373fcf 100644 --- a/data/similar/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/api/SimilarShowsRepository.kt +++ b/data/similar/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/api/SimilarShowsRepository.kt @@ -1,12 +1,13 @@ package com.thomaskioko.tvmaniac.similar.api import com.thomaskioko.tvmaniac.core.db.SimilarShows +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow -import org.mobilenativefoundation.store.store5.StoreReadResponse interface SimilarShowsRepository { suspend fun fetchSimilarShows(traktId: Long): List - fun observeSimilarShows(traktId: Long): Flow>> + fun observeSimilarShows(traktId: Long): Flow>> } diff --git a/data/similar/implementation/build.gradle.kts b/data/similar/implementation/build.gradle.kts index c3bd775d3..788559a46 100644 --- a/data/similar/implementation/build.gradle.kts +++ b/data/similar/implementation/build.gradle.kts @@ -16,6 +16,8 @@ kotlin { implementation(libs.kotlinInject.runtime) implementation(libs.sqldelight.extensions) + implementation(libs.kotlinx.atomicfu) + implementation(libs.store5) } } diff --git a/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/ResponseMapper.kt b/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/ResponseMapper.kt index 37ad2b2b7..e1928c406 100644 --- a/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/ResponseMapper.kt +++ b/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/ResponseMapper.kt @@ -2,12 +2,13 @@ package com.thomaskioko.tvmaniac.similar.implementation import com.thomaskioko.tvmaniac.core.db.Show import com.thomaskioko.tvmaniac.core.db.SimilarShows +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.trakt.api.model.TraktShowResponse fun List.responseToShow(): List { return map { SimilarShows( - trakt_id = it.ids.trakt.toLong(), + id = Id(it.ids.trakt.toLong()), tmdb_id = it.ids.tmdb?.toLong(), title = it.title, overview = it.overview ?: "", @@ -26,7 +27,7 @@ fun List.responseToShow(): List { fun SimilarShows.toShow(): Show { return Show( - trakt_id = trakt_id, + id = id, tmdb_id = tmdb_id, title = title, overview = overview, diff --git a/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowStore.kt b/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowStore.kt index b3acf71e2..d44314b13 100644 --- a/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowStore.kt +++ b/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowStore.kt @@ -1,13 +1,13 @@ package com.thomaskioko.tvmaniac.similar.implementation import com.thomaskioko.tvmaniac.core.db.SimilarShows -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse import com.thomaskioko.tvmaniac.resourcemanager.api.LastRequest import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.shows.api.ShowsDao import com.thomaskioko.tvmaniac.similar.api.SimilarShowsDao import com.thomaskioko.tvmaniac.trakt.api.TraktShowsRemoteDataSource import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.model.ApiResponse import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.Fetcher @@ -23,7 +23,7 @@ class SimilarShowStore( private val requestManagerRepository: RequestManagerRepository, private val scope: AppCoroutineScope, private val logger: KermitLogger, -) : Store> by StoreBuilder.from, List>( +) : Store> by StoreBuilder.from( fetcher = Fetcher.of { id -> when (val apiResult = remoteDataSource.getSimilarShows(id)) { @@ -50,11 +50,11 @@ class SimilarShowStore( writer = { id, list -> list.forEach { - showsDao.insert(it.toShow()) + showsDao.upsert(it.toShow()) - similarShowsDao.insert( - traktId = id, - similarShowId = it.trakt_id, + similarShowsDao.upsert( + similarShowId = it.id.id, + showId = id, ) } diff --git a/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowsDaoImpl.kt b/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowsDaoImpl.kt index 196e255a5..5d7bad51a 100644 --- a/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowsDaoImpl.kt +++ b/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowsDaoImpl.kt @@ -4,6 +4,7 @@ import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import com.thomaskioko.tvmaniac.core.db.SimilarShows import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.similar.api.SimilarShowsDao import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers import kotlinx.coroutines.flow.Flow @@ -15,23 +16,23 @@ class SimilarShowsDaoImpl( private val dispatchers: AppCoroutineDispatchers, ) : SimilarShowsDao { - override fun insert(traktId: Long, similarShowId: Long) { + override fun upsert(showId: Long, similarShowId: Long) { database.similar_showsQueries.transaction { database.similar_showsQueries.insertOrReplace( - id = similarShowId, - trakt_id = traktId, + id = Id(similarShowId), + similar_show_id = Id(showId), ) } } override fun observeSimilarShows(traktId: Long): Flow> { - return database.similar_showsQueries.similarShows(trakt_id = traktId) + return database.similar_showsQueries.similarShows(Id(traktId)) .asFlow() .mapToList(dispatchers.io) } override fun delete(id: Long) { - database.similar_showsQueries.delete(id) + database.similar_showsQueries.delete(Id(id)) } override fun deleteAll() { diff --git a/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowsRepositoryImpl.kt b/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowsRepositoryImpl.kt index 9582d267c..abe793ec4 100644 --- a/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowsRepositoryImpl.kt +++ b/data/similar/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/implementation/SimilarShowsRepositoryImpl.kt @@ -4,14 +4,21 @@ import com.thomaskioko.tvmaniac.core.db.SimilarShows import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.similar.api.SimilarShowsRepository import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.StoreReadRequest -import org.mobilenativefoundation.store.store5.StoreReadResponse import org.mobilenativefoundation.store.store5.impl.extensions.get import kotlin.time.Duration.Companion.days +@OptIn(ExperimentalCoroutinesApi::class) @Inject class SimilarShowsRepositoryImpl( private val store: SimilarShowStore, @@ -21,7 +28,7 @@ class SimilarShowsRepositoryImpl( override suspend fun fetchSimilarShows(traktId: Long): List = store.get(traktId) - override fun observeSimilarShows(traktId: Long): Flow>> = + override fun observeSimilarShows(traktId: Long): Flow>> = store.stream( StoreReadRequest.cached( key = traktId, @@ -32,5 +39,14 @@ class SimilarShowsRepositoryImpl( ), ), ) + .distinctUntilChanged() + .flatMapLatest { + val data = it.dataOrNull() + if (data != null) { + flowOf(Either.Right(data)) + } else { + emptyFlow() + } + } .flowOn(dispatchers.io) } diff --git a/data/similar/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/testing/FakeSimilarShowsRepository.kt b/data/similar/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/testing/FakeSimilarShowsRepository.kt index 9797886d5..81343e06f 100644 --- a/data/similar/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/testing/FakeSimilarShowsRepository.kt +++ b/data/similar/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/similar/testing/FakeSimilarShowsRepository.kt @@ -2,21 +2,22 @@ package com.thomaskioko.tvmaniac.similar.testing import com.thomaskioko.tvmaniac.core.db.SimilarShows import com.thomaskioko.tvmaniac.similar.api.SimilarShowsRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf -import org.mobilenativefoundation.store.store5.StoreReadResponse class FakeSimilarShowsRepository : SimilarShowsRepository { - private var similarShowsResult: Flow>> = flowOf() + private var similarShowsResult: Flow>> = flowOf() - suspend fun setSimilarShowsResult(result: StoreReadResponse>) { + suspend fun setSimilarShowsResult(result: Either>) { similarShowsResult = flow { emit(result) } } override suspend fun fetchSimilarShows(traktId: Long): List = emptyList() - override fun observeSimilarShows(traktId: Long): Flow>> = + override fun observeSimilarShows(traktId: Long): Flow>> = similarShowsResult } diff --git a/data/trailers/api/build.gradle.kts b/data/trailers/api/build.gradle.kts index 694d850e9..417d75bab 100644 --- a/data/trailers/api/build.gradle.kts +++ b/data/trailers/api/build.gradle.kts @@ -7,11 +7,9 @@ kotlin { commonMain { dependencies { api(projects.core.database) - api(projects.core.networkutil) + api(projects.core.util) api(libs.coroutines.core) - api(libs.kotlinx.atomicfu) - api(libs.store5) } } } diff --git a/data/trailers/api/src/commonMain/kotlin/com.thomaskioko.tvmaniac.data.trailers.implementation/TrailerDao.kt b/data/trailers/api/src/commonMain/kotlin/com.thomaskioko.tvmaniac.data.trailers.implementation/TrailerDao.kt index 66abbf9f7..d2e174df3 100644 --- a/data/trailers/api/src/commonMain/kotlin/com.thomaskioko.tvmaniac.data.trailers.implementation/TrailerDao.kt +++ b/data/trailers/api/src/commonMain/kotlin/com.thomaskioko.tvmaniac.data.trailers.implementation/TrailerDao.kt @@ -5,9 +5,9 @@ import kotlinx.coroutines.flow.Flow interface TrailerDao { - fun insert(trailer: Trailers) + fun upsert(trailer: Trailers) - fun insert(trailerList: List) + fun upsert(trailerList: List) fun observeTrailersById(showId: Long): Flow> diff --git a/data/trailers/api/src/commonMain/kotlin/com.thomaskioko.tvmaniac.data.trailers.implementation/TrailerRepository.kt b/data/trailers/api/src/commonMain/kotlin/com.thomaskioko.tvmaniac.data.trailers.implementation/TrailerRepository.kt index ca393b993..1cb15fa65 100644 --- a/data/trailers/api/src/commonMain/kotlin/com.thomaskioko.tvmaniac.data.trailers.implementation/TrailerRepository.kt +++ b/data/trailers/api/src/commonMain/kotlin/com.thomaskioko.tvmaniac.data.trailers.implementation/TrailerRepository.kt @@ -1,11 +1,12 @@ package com.thomaskioko.tvmaniac.data.trailers.implementation import com.thomaskioko.tvmaniac.core.db.Trailers +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow -import org.mobilenativefoundation.store.store5.StoreReadResponse interface TrailerRepository { fun isYoutubePlayerInstalled(): Flow - fun observeTrailersStoreResponse(traktId: Long): Flow>> + fun observeTrailersStoreResponse(traktId: Long): Flow>> suspend fun fetchTrailersByShowId(traktId: Long): List } diff --git a/data/trailers/implementation/build.gradle.kts b/data/trailers/implementation/build.gradle.kts index b3df2065e..dfc9cba1d 100644 --- a/data/trailers/implementation/build.gradle.kts +++ b/data/trailers/implementation/build.gradle.kts @@ -16,6 +16,8 @@ kotlin { implementation(libs.kermit) implementation(libs.kotlinInject.runtime) implementation(libs.sqldelight.extensions) + implementation(libs.kotlinx.atomicfu) + implementation(libs.store5) } } } diff --git a/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerDaoImpl.kt b/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerDaoImpl.kt index 2fd127a33..c9b4fdde8 100644 --- a/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerDaoImpl.kt +++ b/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerDaoImpl.kt @@ -4,6 +4,7 @@ import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import com.thomaskioko.tvmaniac.core.db.Trailers import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers import kotlinx.coroutines.flow.Flow import me.tatarka.inject.annotations.Inject @@ -14,10 +15,10 @@ class TrailerDaoImpl( private val dispatchers: AppCoroutineDispatchers, ) : TrailerDao { - override fun insert(trailer: Trailers) { + override fun upsert(trailer: Trailers) { database.trailersQueries.insertOrReplace( id = trailer.id, - trakt_id = trailer.trakt_id, + show_id = trailer.show_id, key = trailer.key, name = trailer.name, site = trailer.site, @@ -26,19 +27,19 @@ class TrailerDaoImpl( ) } - override fun insert(trailerList: List) { - trailerList.forEach { insert(it) } + override fun upsert(trailerList: List) { + trailerList.forEach { upsert(it) } } override fun observeTrailersById(showId: Long): Flow> { - return database.trailersQueries.selectByShowId(showId) + return database.trailersQueries.selectByShowId(Id(showId)) .asFlow() .mapToList(dispatchers.io) } override fun delete(id: Long) { database.transaction { - database.trailersQueries.delete(id) + database.trailersQueries.delete(Id(id)) } } diff --git a/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerMapper.kt b/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerMapper.kt index ad1b7c77c..f7d2de761 100644 --- a/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerMapper.kt +++ b/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerMapper.kt @@ -1,12 +1,13 @@ package com.thomaskioko.tvmaniac.data.trailers.implementation import com.thomaskioko.tvmaniac.core.db.Trailers +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.tmdb.api.model.TrailerResponse fun List.toEntity(id: Long) = map { trailer -> Trailers( id = trailer.id, - trakt_id = id, + show_id = Id(id), key = trailer.key, name = trailer.name, site = trailer.site, diff --git a/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerRepositoryImpl.kt b/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerRepositoryImpl.kt index 14680fa13..035aa9a07 100644 --- a/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerRepositoryImpl.kt +++ b/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerRepositoryImpl.kt @@ -4,14 +4,21 @@ import com.thomaskioko.tvmaniac.core.db.Trailers import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.util.AppUtils import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.StoreReadRequest -import org.mobilenativefoundation.store.store5.StoreReadResponse import org.mobilenativefoundation.store.store5.impl.extensions.get import kotlin.time.Duration.Companion.days +@OptIn(ExperimentalCoroutinesApi::class) @Inject class TrailerRepositoryImpl( private val store: TrailerStore, @@ -25,7 +32,7 @@ class TrailerRepositoryImpl( override suspend fun fetchTrailersByShowId(traktId: Long): List = store.get(traktId) - override fun observeTrailersStoreResponse(traktId: Long): Flow>> = + override fun observeTrailersStoreResponse(traktId: Long): Flow>> = store.stream( StoreReadRequest.cached( key = traktId, @@ -36,5 +43,14 @@ class TrailerRepositoryImpl( ), ), ) + .distinctUntilChanged() + .flatMapLatest { + val data = it.dataOrNull() + if (data != null) { + flowOf(Either.Right(data)) + } else { + emptyFlow() + } + } .flowOn(dispatchers.io) } diff --git a/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerStore.kt b/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerStore.kt index 8be7294ae..856524041 100644 --- a/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerStore.kt +++ b/data/trailers/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/data/trailers/implementation/TrailerStore.kt @@ -1,12 +1,12 @@ package com.thomaskioko.tvmaniac.data.trailers.implementation import com.thomaskioko.tvmaniac.core.db.Trailers -import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse import com.thomaskioko.tvmaniac.resourcemanager.api.LastRequest import com.thomaskioko.tvmaniac.resourcemanager.api.RequestManagerRepository import com.thomaskioko.tvmaniac.shows.api.ShowsDao import com.thomaskioko.tvmaniac.tmdb.api.TmdbNetworkDataSource import com.thomaskioko.tvmaniac.util.KermitLogger +import com.thomaskioko.tvmaniac.util.model.ApiResponse import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.store.store5.Fetcher @@ -22,7 +22,7 @@ class TrailerStore( private val requestManagerRepository: RequestManagerRepository, private val logger: KermitLogger, private val scope: AppCoroutineScope, -) : Store> by StoreBuilder.from, List>( +) : Store> by StoreBuilder.from( fetcher = Fetcher.of { id -> val show = showsDao.getTvShow(id) @@ -49,10 +49,10 @@ class TrailerStore( sourceOfTruth = SourceOfTruth.of( reader = { id -> trailerDao.observeTrailersById(id) }, writer = { id, list -> - trailerDao.insert(list) + trailerDao.upsert(list) requestManagerRepository.insert( LastRequest( - id = list.first().trakt_id, + id = list.first().show_id.id, entityId = id, requestType = "TRAILERS", ), diff --git a/data/trailers/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trailers/testing/FakeTrailerRepository.kt b/data/trailers/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trailers/testing/FakeTrailerRepository.kt index ffd250948..412f30428 100644 --- a/data/trailers/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trailers/testing/FakeTrailerRepository.kt +++ b/data/trailers/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trailers/testing/FakeTrailerRepository.kt @@ -2,28 +2,30 @@ package com.thomaskioko.tvmaniac.trailers.testing import com.thomaskioko.tvmaniac.core.db.Trailers import com.thomaskioko.tvmaniac.data.trailers.implementation.TrailerRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf -import org.mobilenativefoundation.store.store5.StoreReadResponse +import kotlinx.coroutines.flow.receiveAsFlow class FakeTrailerRepository : TrailerRepository { - private var trailerList = mutableListOf() - private var trailersStoreResponse: Flow>> = flowOf() + private var trailerList: Channel> = Channel(Channel.UNLIMITED) + private var trailersStoreResponse: Channel>> = + Channel(Channel.UNLIMITED) - suspend fun setTrailerResult(result: StoreReadResponse>) { - trailersStoreResponse = flow { emit(result) } + suspend fun setTrailerResult(result: Either>) { + trailersStoreResponse.send(result) } - fun setTrailerList(list: List) { - trailerList.clear() - trailerList.addAll(list.toMutableList()) + suspend fun setTrailerList(list: List) { + trailerList.send(list) } override fun isYoutubePlayerInstalled(): Flow = flowOf() - override fun observeTrailersStoreResponse(traktId: Long): Flow>> = - trailersStoreResponse + override fun observeTrailersStoreResponse(traktId: Long): Flow>> = + trailersStoreResponse.receiveAsFlow() - override suspend fun fetchTrailersByShowId(traktId: Long): List = trailerList + override suspend fun fetchTrailersByShowId(traktId: Long): List = trailerList.receive() } diff --git a/data/trailers/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trailers/testing/MockData.kt b/data/trailers/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trailers/testing/MockData.kt index daf967642..4c6e9601e 100644 --- a/data/trailers/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trailers/testing/MockData.kt +++ b/data/trailers/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trailers/testing/MockData.kt @@ -1,11 +1,12 @@ package com.thomaskioko.tvmaniac.trailers.testing import com.thomaskioko.tvmaniac.core.db.Trailers +import com.thomaskioko.tvmaniac.db.Id val trailers = listOf( Trailers( id = "1231", - trakt_id = 84958, + show_id = Id(84958), key = "Fd43V", name = "Some title", site = "Youtube", diff --git a/data/watchlist/api/build.gradle.kts b/data/watchlist/api/build.gradle.kts index ea55b9cd9..417d75bab 100644 --- a/data/watchlist/api/build.gradle.kts +++ b/data/watchlist/api/build.gradle.kts @@ -7,7 +7,7 @@ kotlin { commonMain { dependencies { api(projects.core.database) - api(projects.core.networkutil) + api(projects.core.util) api(libs.coroutines.core) } diff --git a/data/watchlist/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/WatchlistDao.kt b/data/watchlist/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/WatchlistDao.kt index 1367b6fe3..84425e9f4 100644 --- a/data/watchlist/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/WatchlistDao.kt +++ b/data/watchlist/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/WatchlistDao.kt @@ -1,20 +1,20 @@ package com.thomaskioko.tvmaniac.shows.api -import com.thomaskioko.tvmaniac.core.db.SelectWatchlist +import com.thomaskioko.tvmaniac.core.db.WatchedShow import com.thomaskioko.tvmaniac.core.db.Watchlist import kotlinx.coroutines.flow.Flow interface WatchlistDao { - fun insert(followedShow: Watchlist) + fun upsert(watchlist: Watchlist) - fun insert(followedShows: List) + fun upsert(watchedShowList: List) - fun getWatchlist(): List + fun getWatchedShows(): List fun getUnSyncedShows(): List - fun observeWatchlist(): Flow> + fun observeWatchedShows(): Flow> fun updateShowSyncState(traktId: Long) diff --git a/data/watchlist/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/WatchlistRepository.kt b/data/watchlist/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/WatchlistRepository.kt index b51adbc93..802fbffd4 100644 --- a/data/watchlist/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/WatchlistRepository.kt +++ b/data/watchlist/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/shows/api/WatchlistRepository.kt @@ -1,15 +1,15 @@ package com.thomaskioko.tvmaniac.shows.api -import com.thomaskioko.tvmaniac.core.db.SelectWatchlist -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure +import com.thomaskioko.tvmaniac.core.db.WatchedShow +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow interface WatchlistRepository { - fun observeWatchList(): Flow>> + fun observeWatchList(): Flow>> - fun getWatchlist(): List + suspend fun getWatchlist(): List suspend fun updateWatchlist(traktId: Long, addToWatchList: Boolean) diff --git a/data/watchlist/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/implementation/WatchlistDaoImpl.kt b/data/watchlist/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/implementation/WatchlistDaoImpl.kt index 8bee0ddc9..e3036a7c8 100644 --- a/data/watchlist/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/implementation/WatchlistDaoImpl.kt +++ b/data/watchlist/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/implementation/WatchlistDaoImpl.kt @@ -2,9 +2,10 @@ package com.thomaskioko.tvmaniac.watchlist.implementation import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList -import com.thomaskioko.tvmaniac.core.db.SelectWatchlist import com.thomaskioko.tvmaniac.core.db.TvManiacDatabase +import com.thomaskioko.tvmaniac.core.db.WatchedShow import com.thomaskioko.tvmaniac.core.db.Watchlist +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.shows.api.WatchlistDao import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers import kotlinx.coroutines.flow.Flow @@ -16,41 +17,41 @@ class WatchlistDaoImpl( private val dispatchers: AppCoroutineDispatchers, ) : WatchlistDao { - override fun insert(followedShow: Watchlist) { + override fun upsert(watchlist: Watchlist) { database.transaction { database.watchlistQueries.insertOrReplace( - id = followedShow.id, - synced = followedShow.synced, - created_at = followedShow.created_at, + id = watchlist.id, + synced = watchlist.synced, + created_at = watchlist.created_at, ) } } - override fun insert(followedShows: List) { - followedShows.forEach { insert(it) } + override fun upsert(watchedShowList: List) { + watchedShowList.forEach { upsert(it) } } - override fun getWatchlist(): List = - database.watchlistQueries.selectWatchlist() + override fun getWatchedShows(): List = + database.watchlistQueries.watchedShow() .executeAsList() - override fun getUnSyncedShows(): List = - database.watchlistQueries.selectUnsyncedShows() - .executeAsList() - - override fun observeWatchlist(): Flow> = - database.watchlistQueries.selectWatchlist() + override fun observeWatchedShows(): Flow> = + database.watchlistQueries.watchedShow() .asFlow() .mapToList(dispatchers.io) + override fun getUnSyncedShows(): List = + database.watchlistQueries.unsyncedShows() + .executeAsList() + override fun updateShowSyncState(traktId: Long) { database.watchlistQueries.updateFollowedState( - id = traktId, + id = Id(traktId), synced = true, ) } override fun removeShow(traktId: Long) { - database.watchlistQueries.removeShow(traktId) + database.watchlistQueries.removeShowFromWatchlist(Id(traktId)) } } diff --git a/data/watchlist/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/implementation/WatchlistRepositoryImpl.kt b/data/watchlist/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/implementation/WatchlistRepositoryImpl.kt index 4563d8175..b12486c5b 100644 --- a/data/watchlist/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/implementation/WatchlistRepositoryImpl.kt +++ b/data/watchlist/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/implementation/WatchlistRepositoryImpl.kt @@ -1,17 +1,18 @@ package com.thomaskioko.tvmaniac.watchlist.implementation -import com.thomaskioko.tvmaniac.core.db.SelectWatchlist +import com.thomaskioko.tvmaniac.core.db.WatchedShow import com.thomaskioko.tvmaniac.core.db.Watchlist -import com.thomaskioko.tvmaniac.core.networkutil.DefaultError -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure -import com.thomaskioko.tvmaniac.core.networkutil.NetworkExceptionHandler +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.profile.api.ProfileDao import com.thomaskioko.tvmaniac.shows.api.WatchlistDao import com.thomaskioko.tvmaniac.shows.api.WatchlistRepository import com.thomaskioko.tvmaniac.trakt.api.TraktListRemoteDataSource import com.thomaskioko.tvmaniac.util.DateFormatter +import com.thomaskioko.tvmaniac.util.NetworkExceptionHandler import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers +import com.thomaskioko.tvmaniac.util.model.DefaultError +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.distinctUntilChanged @@ -20,7 +21,7 @@ import kotlinx.coroutines.flow.map import me.tatarka.inject.annotations.Inject @Inject -class WatchlistRepositoryImpl constructor( +class WatchlistRepositoryImpl( private val remoteDataSource: TraktListRemoteDataSource, private val watchlistDao: WatchlistDao, private val profileDao: ProfileDao, @@ -36,9 +37,9 @@ class WatchlistRepositoryImpl constructor( if (user.slug.isNotBlank()) { watchlistDao.getUnSyncedShows() .map { - remoteDataSource.addShowToWatchList(it.id) + remoteDataSource.addShowToWatchList(it.id.id) - watchlistDao.insert( + watchlistDao.upsert( Watchlist( id = it.id, synced = true, @@ -53,9 +54,9 @@ class WatchlistRepositoryImpl constructor( override suspend fun updateWatchlist(traktId: Long, addToWatchList: Boolean) { // TODO:: Check if user is signed into trakt and sync followed shows. when { - addToWatchList -> watchlistDao.insert( + addToWatchList -> watchlistDao.upsert( Watchlist( - id = traktId, + id = Id(traktId), synced = false, created_at = dateFormatter.getTimestampMilliseconds(), ), @@ -65,11 +66,11 @@ class WatchlistRepositoryImpl constructor( } } - override fun observeWatchList(): Flow>> = - watchlistDao.observeWatchlist() + override fun observeWatchList(): Flow>> = + watchlistDao.observeWatchedShows() .distinctUntilChanged() .map { Either.Right(it) } .catch { Either.Left(DefaultError(exceptionHandler.resolveError(it))) } - override fun getWatchlist(): List = watchlistDao.getWatchlist() + override suspend fun getWatchlist(): List = watchlistDao.getWatchedShows() } diff --git a/data/watchlist/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/testing/FakeWatchlistRepository.kt b/data/watchlist/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/testing/FakeWatchlistRepository.kt index b39f612a5..c8ff4d5da 100644 --- a/data/watchlist/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/testing/FakeWatchlistRepository.kt +++ b/data/watchlist/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/testing/FakeWatchlistRepository.kt @@ -1,25 +1,27 @@ package com.thomaskioko.tvmaniac.watchlist.testing -import com.thomaskioko.tvmaniac.core.db.SelectWatchlist -import com.thomaskioko.tvmaniac.core.networkutil.Either -import com.thomaskioko.tvmaniac.core.networkutil.Failure +import com.thomaskioko.tvmaniac.core.db.WatchedShow import com.thomaskioko.tvmaniac.shows.api.WatchlistRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.receiveAsFlow class FakeWatchlistRepository : WatchlistRepository { - private var watchlistResult = flowOf>>() + private var watchlist: Channel> = Channel(Channel.UNLIMITED) + private var watchlistResult: Channel>> = + Channel(Channel.UNLIMITED) - suspend fun setFollowedResult(result: Either>) { - watchlistResult = flow { emit(result) } + suspend fun setFollowedResult(result: List) { + watchlist.send(result) } - override fun observeWatchList(): Flow>> = - watchlistResult + override fun observeWatchList(): Flow>> = + watchlistResult.receiveAsFlow() - override fun getWatchlist(): List = com.thomaskioko.tvmaniac.watchlist.testing.watchlistResult + override suspend fun getWatchlist(): List = watchlist.receive() override suspend fun updateWatchlist(traktId: Long, addToWatchList: Boolean) { } diff --git a/data/watchlist/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/testing/MockData.kt b/data/watchlist/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/testing/MockData.kt index 913e0dd7f..8216a1b7a 100644 --- a/data/watchlist/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/testing/MockData.kt +++ b/data/watchlist/testing/src/commonMain/kotlin/com/thomaskioko/tvmaniac/watchlist/testing/MockData.kt @@ -1,10 +1,11 @@ package com.thomaskioko.tvmaniac.watchlist.testing -import com.thomaskioko.tvmaniac.core.db.SelectWatchlist +import com.thomaskioko.tvmaniac.core.db.WatchedShow +import com.thomaskioko.tvmaniac.db.Id val watchlistResult = listOf( - SelectWatchlist( - trakt_id = 84958, + WatchedShow( + show_id = Id(84958), tmdb_id = 849583, title = "Loki", overview = "After stealing the Tesseract during the events of “Avengers: Endgame,” " + @@ -23,10 +24,6 @@ val watchlistResult = listOf( poster_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", backdrop_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", aired_episodes = 12, - id = 84958, - synced = true, created_at = 12345645, - trakt_id_ = 1232, - tmdb_id_ = 849583, ), ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75f718b49..9da524c3a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,89 +1,72 @@ [versions] accompanist = "0.33.2-alpha" -agp = "8.1.2" -appauth = "0.8.1" -atomicfu = "0.21.0" - -androidx-activity = "1.8.0" -androidx-appCompat = "1.5.1" -androidx-browser = "1.6.0" +agp = "8.1.4" +androidx-activity = "1.8.1" +androidx-browser = "1.7.0" androidx-core = "1.12.0" androidx-core-splashscreen = "1.0.1" -androidx-datastore = "1.1.0-alpha05" +androidx-datastore = "1.1.0-alpha06" androidx-lifecycle = "2.6.2" androidx-material3 = "1.1.2" -androidx-navigation = "2.7.4" +androidx-navigation = "2.7.5" androidx-palette = "1.0.0" -androidx-paging = "3.2.1" androidx-work = "2.8.1" - -coil = "2.4.0" +appauth = "0.11.1" +atomicfu = "0.22.0" +coil = "2.5.0" compose-bom = "2023.10.01" -compose-constraintlayout= "1.0.1" +compose-constraintlayout = "1.0.1" compose-paging = "3.2.1" -compose-material = "1.5.4" -composecompiler = "1.5.3" +composecompiler = "1.5.4" coroutines = "1.7.3" -datetime = "0.4.0" -detekt = "1.22.0" -dependency-analysis = "1.13.1" -dependency-check = "0.44.0" -desugar = "2.0.3" +datetime = "0.4.1" +dependency-analysis = "1.25.0" +dependency-check = "0.49.0" +desugar = "2.0.4" +detekt = "1.23.3" flowredux = "1.2.0" -junit = "4.13.2" kenburns = "1.0.7" -kermit = "1.2.2" +kermit = "1.2.3" kmmbridge = "0.3.7" kotest = "5.5.4" -kotlin = "1.9.10" -kotlinx-collections = "0.3.5" -kotlininject = "0.6.1" -ktor = "2.3.2" -ksp = "1.9.10-1.0.13" +kotlin = "1.9.20" +kotlininject = "0.6.3" +kotlinx-collections = "0.3.6" +ksp = "1.9.20-1.0.13" +ktor = "2.3.6" lint = "1.2.0" +moko-resources = "0.23.0" shared-module-version = "0.8.0" snapper = "0.3.0" -sqldelight = "2.0.0-rc01" -store5 = "5.0.0-beta01" +sqldelight = "2.0.0" +store5 = "5.0.0" turbine = "1.0.0" yamlkt = "0.12.0" youtubePlayer = "11.0.1" -android-compileSdk = "34" -android-minSdk = "21" - [libraries] -android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar" } -android-gradle-tools = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } -kotlin-gradle = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } - 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" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-ui = { module = "androidx.compose.ui:ui" } -androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling"} -androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util"} - +androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" } androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } -androidx-browser = { module = "androidx.browser:browser", version.ref = "androidx-browser" } - +androidx-browser = { module = "androidx.browser:browser", version.ref = "androidx-browser" } androidx-compose-activity = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-compose-constraintlayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "compose-constraintlayout" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-material3" } -androidx-compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "androidx-material3" } androidx-compose-paging = { module = "androidx.paging:paging-compose", version.ref = "compose-paging" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-core-splashscreen" } androidx-datastore-preference = { module = "androidx.datastore:datastore-preferences-core", version.ref = "androidx-datastore" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } -androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } -androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } androidx-navigation-common = { module = "androidx.navigation:navigation-common", version.ref = "androidx-navigation" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } androidx-navigation-runtime = { module = "androidx.navigation:navigation-runtime", version.ref = "androidx-navigation" } -androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "androidx-paging" } androidx-palette = { module = "androidx.palette:palette-ktx", version.ref = "androidx-palette" } androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" } @@ -97,12 +80,8 @@ coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", ve coroutines-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "coroutines" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } -desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" } - flowredux = { module = "com.freeletics.flowredux:flowredux", version.ref = "flowredux" } -junit = { module = "junit:junit", version.ref = "junit" } - kenburns = { module = "com.flaviofaria:kenburnsview", version.ref = "kenburns" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } @@ -123,12 +102,14 @@ ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", lint-compose = { module = "com.slack.lint.compose:compose-lint-checks", version.ref = "lint" } +moko-core = { module = "dev.icerock.moko:resources", version.ref = "moko-resources" } + snapper = { module = "dev.chrisbanes.snapper:snapper", version.ref = "snapper" } -sqldelight-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-driver-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" } +sqldelight-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqldelight-primitive-adapters = { module = "app.cash.sqldelight:primitive-adapters", version.ref = "sqldelight" } store5 = { module = "org.mobilenativefoundation.store:store5", version.ref = "store5" } @@ -136,16 +117,21 @@ turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } yamlkt = { module = "net.mamoe.yamlkt:yamlkt", version.ref = "yamlkt" } youtubePlayer = { module = "com.pierfrancescosoffritti.androidyoutubeplayer:core", version.ref = "youtubePlayer" } +# Build logic dependencies +android-desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" } +android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } +kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } -dependency-check = { id = "com.github.ben-manes.versions", version.ref = "dependency-check" } dependency-analysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependency-analysis" } +dependency-check = { id = "com.github.ben-manes.versions", version.ref = "dependency-check" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } kmmbridge = { id = "co.touchlab.faktory.kmmbridge", version.ref = "kmmbridge" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +moko-resources = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko-resources" } multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } diff --git a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsMapper.kt b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsMapper.kt index 92f6a8131..915879701 100644 --- a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsMapper.kt +++ b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsMapper.kt @@ -2,16 +2,17 @@ package com.thomaskioko.tvmaniac.presentation.discover import com.thomaskioko.tvmaniac.core.db.ShowsByCategory import com.thomaskioko.tvmaniac.presentation.discover.model.TvShow +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import org.mobilenativefoundation.store.store5.StoreReadResponse fun List?.toTvShowList(): ImmutableList = this?.map { it.toTvShow() }?.toImmutableList() ?: persistentListOf() fun ShowsByCategory.toTvShow(): TvShow = TvShow( - traktId = trakt_id, + traktId = id.id, tmdbId = tmdb_id, title = title, overview = overview, @@ -25,26 +26,10 @@ fun ShowsByCategory.toTvShow(): TvShow = TvShow( status = status, ) -fun toShowResultState( - trending: StoreReadResponse>, - popular: StoreReadResponse>, - anticipated: StoreReadResponse>, - recommended: StoreReadResponse>, -): DataLoaded = DataLoaded( - trendingShows = trending.dataOrNull().toTvShowList(), - popularShows = popular.dataOrNull().toTvShowList(), - anticipatedShows = anticipated.dataOrNull().toTvShowList(), - recommendedShows = recommended.dataOrNull()?.take(5).toTvShowList(), - errorMessage = getErrorMessage(trending, popular, anticipated, recommended), - isContentEmpty = trending.dataOrNull().isNullOrEmpty() && - popular.dataOrNull().isNullOrEmpty() && anticipated.dataOrNull().isNullOrEmpty() && - recommended.dataOrNull().isNullOrEmpty(), -) - -private fun getErrorMessage( - trending: StoreReadResponse>, - popular: StoreReadResponse>, - anticipated: StoreReadResponse>, - recommended: StoreReadResponse>, -) = trending.errorMessageOrNull() ?: popular.errorMessageOrNull() - ?: anticipated.errorMessageOrNull() ?: recommended.errorMessageOrNull() +fun getErrorMessage( + trending: Either>, + popular: Either>, + anticipated: Either>, + recommended: Either>, +) = trending.getErrorOrNull()?.errorMessage ?: popular.getErrorOrNull()?.errorMessage + ?: anticipated.getErrorOrNull()?.errorMessage ?: recommended.getErrorOrNull()?.errorMessage diff --git a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsState.kt b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsState.kt index 702172175..9582e3b59 100644 --- a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsState.kt +++ b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverShowsState.kt @@ -2,16 +2,17 @@ package com.thomaskioko.tvmaniac.presentation.discover import com.thomaskioko.tvmaniac.presentation.discover.model.TvShow import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf sealed interface DiscoverState -object Loading : DiscoverState +data object Loading : DiscoverState +data class ErrorState(val errorMessage: String?) : DiscoverState data class DataLoaded( - val recommendedShows: ImmutableList? = null, - val trendingShows: ImmutableList? = null, - val popularShows: ImmutableList? = null, - val anticipatedShows: ImmutableList? = null, + val recommendedShows: ImmutableList = persistentListOf(), + val trendingShows: ImmutableList = persistentListOf(), + val popularShows: ImmutableList = persistentListOf(), + val anticipatedShows: ImmutableList = persistentListOf(), val errorMessage: String? = null, - val isContentEmpty: Boolean = true, ) : DiscoverState diff --git a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachine.kt b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachine.kt index dc8ebb7b5..e9eb1fac5 100644 --- a/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachine.kt +++ b/presentation/discover/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachine.kt @@ -8,6 +8,7 @@ import com.thomaskioko.tvmaniac.showimages.api.ShowImagesRepository import com.thomaskioko.tvmaniac.shows.api.DiscoverRepository import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import me.tatarka.inject.annotations.Inject @@ -35,15 +36,10 @@ class DiscoverStateMachine( popularShows = result.popularShows, anticipatedShows = result.anticipatedShows, errorMessage = result.errorMessage, - isContentEmpty = result.isContentEmpty, ) } } - collectWhileInStateEffect(showImagesRepository.updateShowArtWork()) { _, _ -> - /** No need to do anything. Just trigger artwork download. **/ - } - on { _, state -> // TODO:: Implement reloading category data state.noChange() @@ -52,7 +48,9 @@ class DiscoverStateMachine( on { _, state -> state.override { Loading } } + } + inState { on { _, state -> state.mutate { copy(errorMessage = null) @@ -80,11 +78,19 @@ class DiscoverStateMachine( private fun observeShowData(): Flow = combine( - discoverRepository.observeTrendingShows(), - discoverRepository.observePopularShows(), - discoverRepository.observeAnticipatedShows(), - discoverRepository.observeRecommendedShows(), - ) { trending, popular, anticipated, featured -> - toShowResultState(trending, popular, anticipated, featured) + discoverRepository.observeShowCategory(Category.TRENDING), + discoverRepository.observeShowCategory(Category.POPULAR), + discoverRepository.observeShowCategory(Category.ANTICIPATED), + discoverRepository.observeShowCategory(Category.RECOMMENDED), + showImagesRepository.updateShowArtWork(), + ) { trending, popular, anticipated, recommended, _ -> + DataLoaded( + trendingShows = trending.getOrNull().toTvShowList(), + popularShows = popular.getOrNull().toTvShowList(), + anticipatedShows = anticipated.getOrNull().toTvShowList(), + recommendedShows = recommended.getOrNull()?.take(5).toTvShowList(), + errorMessage = getErrorMessage(trending, popular, anticipated, recommended), + ) } + .catch { ErrorState(errorMessage = it.message) } } diff --git a/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachineTest.kt b/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachineTest.kt index cfea3517f..ff0ea5355 100644 --- a/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachineTest.kt +++ b/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/DiscoverStateMachineTest.kt @@ -5,84 +5,24 @@ import com.thomaskioko.tvmaniac.shows.testing.FakeDiscoverRepository import com.thomaskioko.tvmaniac.tmdb.testing.FakeShowImagesRepository import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runTest -import org.mobilenativefoundation.store.store5.StoreReadResponse -import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin -import kotlin.test.Ignore import kotlin.test.Test -@Ignore internal class DiscoverStateMachineTest { - private val traktRepository = FakeDiscoverRepository() + private val discoverRepository = FakeDiscoverRepository() private val imagesRepository = FakeShowImagesRepository() - private val stateMachine = DiscoverStateMachine( - traktRepository, - imagesRepository, - ) + private val stateMachine = DiscoverStateMachine(discoverRepository, imagesRepository) @Test - fun initial_state_emits_expected_result() = runTest { - traktRepository.setTrendingResult( - StoreReadResponse.Data( - value = categoryResult(1), - origin = StoreReadResponseOrigin.Cache, - ), - ) - traktRepository.setPopularResult( - StoreReadResponse.Data( - value = categoryResult(2), - origin = StoreReadResponseOrigin.Cache, - ), - ) - traktRepository.setAnticipatedResult( - StoreReadResponse.Data( - value = categoryResult(3), - origin = StoreReadResponseOrigin.Cache, - ), - ) - traktRepository.setFeaturedResult( - StoreReadResponse.Data( - value = categoryResult(4), - origin = StoreReadResponseOrigin.Cache, - ), - ) + fun `given an result is loaded then correct state is emitted`() = runTest { + discoverRepository.setShowCategory(categoryResult(1)) + discoverRepository.setShowCategory(categoryResult(2)) + discoverRepository.setShowCategory(categoryResult(3)) + discoverRepository.setShowCategory(categoryResult(4)) stateMachine.state.test { awaitItem() shouldBe Loading awaitItem() shouldBe discoverContent } } - - @Test - fun on_category_error_emits_expected_result() = runTest { - traktRepository.setFeaturedResult( - StoreReadResponse.Error.Message( - message = "Something went wrong", - origin = StoreReadResponseOrigin.Cache, - ), - ) - traktRepository.setAnticipatedResult( - StoreReadResponse.Error.Message( - message = "Something went wrong", - origin = StoreReadResponseOrigin.Cache, - ), - ) - traktRepository.setPopularResult( - StoreReadResponse.Error.Message( - message = "Something went wrong", - origin = StoreReadResponseOrigin.Cache, - ), - ) - traktRepository.setTrendingResult( - StoreReadResponse.Error.Message( - message = "Something went wrong", - origin = StoreReadResponseOrigin.Cache, - ), - ) - - stateMachine.state.test { - awaitItem() shouldBe Loading - awaitItem() shouldBe DataLoaded() - } - } } diff --git a/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/MockData.kt b/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/MockData.kt index d316a7a23..910e5ecda 100644 --- a/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/MockData.kt +++ b/presentation/discover/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/discover/MockData.kt @@ -1,6 +1,7 @@ package com.thomaskioko.tvmaniac.presentation.discover import com.thomaskioko.tvmaniac.core.db.ShowsByCategory +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.presentation.discover.model.TvShow import kotlinx.collections.immutable.toImmutableList @@ -33,7 +34,7 @@ val discoverContent = DataLoaded( fun categoryResult(categoryId: Long) = listOf( ShowsByCategory( - trakt_id = 84958, + id = Id(84958), tmdb_id = 849583, title = "Loki", overview = "After stealing the Tesseract during the events of “Avengers: Endgame,” " + @@ -51,7 +52,32 @@ fun categoryResult(categoryId: Long) = listOf( runtime = 45, poster_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", backdrop_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", - category_id = categoryId, + category_id = Id(categoryId), aired_episodes = null, ), ) + +fun updateCategoryResult(categoryId: Long, size: Int = 1) = List(size) { + ShowsByCategory( + id = Id(84958), + tmdb_id = 849583, + title = "Loki", + overview = "After stealing the Tesseract during the events of “Avengers: Endgame,” " + + "an alternate version of Loki is brought to the mysterious Time Variance " + + "Authority, a bureaucratic organization that exists outside of time and " + + "space and monitors the timeline. They give Loki a choice: face being " + + "erased from existence due to being a “time variant”or help fix " + + "the timeline and stop a greater threat.", + language = "en", + votes = 4958, + rating = 8.1, + genres = listOf("Horror", "Action"), + status = "Returning Series", + year = "2024", + runtime = 45, + poster_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", + backdrop_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", + category_id = Id(categoryId), + aired_episodes = null, + ) +} diff --git a/presentation/profile/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfileStateMachine.kt b/presentation/profile/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfileStateMachine.kt index d08011fff..ac240eee9 100644 --- a/presentation/profile/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfileStateMachine.kt +++ b/presentation/profile/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfileStateMachine.kt @@ -5,9 +5,9 @@ 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.Either import kotlinx.coroutines.ExperimentalCoroutinesApi import me.tatarka.inject.annotations.Inject -import org.mobilenativefoundation.store.store5.StoreReadResponse @OptIn(ExperimentalCoroutinesApi::class) @Inject @@ -72,38 +72,23 @@ class ProfileStateMachine( collectWhileInState(profileRepository.observeProfile("me")) { response, state -> when (response) { - is StoreReadResponse.NoNewData -> state.noChange() - is StoreReadResponse.Loading -> state.mutate { + is Either.Left -> state.mutate { copy( - isLoading = true, + isLoading = false, + errorMessage = response.error.errorMessage, ) } - - is StoreReadResponse.Data -> state.mutate { + is Either.Right -> state.mutate { copy( isLoading = false, userInfo = UserInfo( - slug = response.requireData().slug, - userName = response.requireData().user_name, - fullName = response.requireData().full_name, - userPicUrl = response.requireData().profile_picture, + slug = response.data.slug, + userName = response.data.user_name, + fullName = response.data.full_name, + userPicUrl = response.data.profile_picture, ), ) } - - is StoreReadResponse.Error.Exception -> state.mutate { - copy( - isLoading = false, - errorMessage = response.error.message, - ) - } - - is StoreReadResponse.Error.Message -> state.mutate { - copy( - isLoading = false, - errorMessage = response.message, - ) - } } } diff --git a/presentation/profile/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfileStateMachineTest.kt b/presentation/profile/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfileStateMachineTest.kt index 0d2898fa3..caae38e4f 100644 --- a/presentation/profile/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfileStateMachineTest.kt +++ b/presentation/profile/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/profile/ProfileStateMachineTest.kt @@ -7,10 +7,10 @@ import com.thomaskioko.tvmaniac.trakt.profile.testing.FakeProfileRepository import com.thomaskioko.tvmaniac.trakt.profile.testing.user import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthState import com.thomaskioko.tvmaniac.traktauth.testing.FakeTraktAuthRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.ServerError import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runTest -import org.mobilenativefoundation.store.store5.StoreReadResponse -import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin import kotlin.test.Test class ProfileStateMachineTest { @@ -53,12 +53,7 @@ class ProfileStateMachineTest { traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) datastoreRepository.setAuthState(authenticatedAuthState) - profileRepository.setUserData( - StoreReadResponse.Data( - value = user, - origin = StoreReadResponseOrigin.Cache, - ), - ) + profileRepository.setUserData(Either.Right(user)) awaitItem() shouldBe LoggedInContent() awaitItem() shouldBe LoggedInContent() @@ -96,10 +91,7 @@ class ProfileStateMachineTest { traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) datastoreRepository.setAuthState(authenticatedAuthState) profileRepository.setUserData( - StoreReadResponse.Error.Exception( - error = Throwable(errorMessage), - origin = StoreReadResponseOrigin.Cache, - ), + Either.Left(ServerError(errorMessage)), ) awaitItem() shouldBe LoggedInContent() @@ -117,12 +109,7 @@ class ProfileStateMachineTest { traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) datastoreRepository.setAuthState(authenticatedAuthState) - profileRepository.setUserData( - StoreReadResponse.Data( - value = user, - origin = StoreReadResponseOrigin.Cache, - ), - ) + profileRepository.setUserData(Either.Right(user)) awaitItem() shouldBe LoggedInContent() awaitItem() shouldBe LoggedInContent() diff --git a/presentation/seasondetails/build.gradle.kts b/presentation/seasondetails/build.gradle.kts index 797a389a7..1fb41a044 100644 --- a/presentation/seasondetails/build.gradle.kts +++ b/presentation/seasondetails/build.gradle.kts @@ -13,6 +13,8 @@ kotlin { implementation(projects.data.episodes.api) implementation(projects.data.seasondetails.api) + api(libs.kotlinx.collections) + implementation(libs.flowredux) implementation(libs.kotlinInject.runtime) } diff --git a/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/Mapper.kt b/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/Mapper.kt index bfff1f713..ec0b75584 100644 --- a/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/Mapper.kt +++ b/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/Mapper.kt @@ -1,42 +1,36 @@ package com.thomaskioko.tvmaniac.presentation.seasondetails -import com.thomaskioko.tvmaniac.core.db.SeasonWithEpisodes -import com.thomaskioko.tvmaniac.core.networkutil.Either +import com.thomaskioko.tvmaniac.core.db.SeasonEpisodeDetailsById import com.thomaskioko.tvmaniac.presentation.seasondetails.model.Episode import com.thomaskioko.tvmaniac.presentation.seasondetails.model.SeasonDetails +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList -fun Either.Right>.toSeasonWithEpisodes(): List { - return data?.groupBy { it.name }?.map { groupMap -> - SeasonDetails( - seasonId = groupMap.value.first().season_id, - seasonName = groupMap.key, - episodes = groupMap.value.map { it.toEpisode() }, - episodeCount = groupMap.value.size.toLong(), - watchProgress = 0f, // TODO:: Fetch watch progress - ) - } ?: emptyList() +fun List?.toSeasonWithEpisodes(): PersistentList { + return this + ?.groupBy { it.season_id } + ?.map { (_, groupMap) -> + val seasonRow = groupMap.first() + SeasonDetails( + seasonId = seasonRow.season_id.id, + seasonName = seasonRow.season_title, + episodes = groupMap.map { it.toEpisode() }.toImmutableList(), + episodeCount = seasonRow.episode_count, + watchProgress = 0f, + ) + }?.toPersistentList() ?: persistentListOf() } -fun List?.toSeasonWithEpisodes(): List { - return this?.groupBy { it.name }?.map { groupMap -> - SeasonDetails( - seasonId = groupMap.value.first().season_id, - seasonName = groupMap.key, - episodes = groupMap.value.map { it.toEpisode() }, - episodeCount = groupMap.value.size.toLong(), - watchProgress = 0f, // TODO:: Fetch watch progress - ) - } ?: emptyList() -} - -fun SeasonWithEpisodes.toEpisode(): Episode { +fun SeasonEpisodeDetailsById.toEpisode(): Episode { return Episode( - id = id, - seasonId = season_id, - episodeTitle = title_, - episodeNumberTitle = "E$episode_number • $title_", + id = episode_id.id, + seasonId = season_id.id, + episodeTitle = episode_title, + episodeNumberTitle = "E$episode_number • $episode_title", overview = overview, - imageUrl = image_url, + imageUrl = episode_image_url, runtime = runtime, voteCount = votes, episodeNumber = episode_number, @@ -48,8 +42,4 @@ fun SeasonWithEpisodes.toEpisode(): Episode { ) } -fun Either.Right>.getTitle(): String = - data?.firstOrNull()?.title ?: "" - -fun List?.getTitle(): String = - this?.firstOrNull()?.title ?: "" +fun List?.getTitle(): String = this?.firstOrNull()?.show_title ?: "" diff --git a/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsState.kt b/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsState.kt index bd25a053e..449f31bcf 100644 --- a/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsState.kt +++ b/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsState.kt @@ -1,14 +1,17 @@ package com.thomaskioko.tvmaniac.presentation.seasondetails import com.thomaskioko.tvmaniac.presentation.seasondetails.model.SeasonDetails +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf sealed interface SeasonDetailsState -object Loading : SeasonDetailsState +data object Loading : SeasonDetailsState data class SeasonDetailsLoaded( val showTitle: String = "", - val seasonDetailsList: List = emptyList(), + val seasonDetailsList: PersistentList = persistentListOf(), + val errorMessage: String? = null, ) : SeasonDetailsState data class LoadingError(val message: String? = null) : SeasonDetailsState diff --git a/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsStateMachine.kt b/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsStateMachine.kt index bde1c8bff..3d5c3caff 100644 --- a/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsStateMachine.kt +++ b/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/SeasonDetailsStateMachine.kt @@ -1,9 +1,6 @@ package com.thomaskioko.tvmaniac.presentation.seasondetails -import com.freeletics.flowredux.dsl.ChangedState import com.freeletics.flowredux.dsl.FlowReduxStateMachine -import com.freeletics.flowredux.dsl.State -import com.thomaskioko.tvmaniac.core.networkutil.Either import com.thomaskioko.tvmaniac.episodeimages.api.EpisodeImageRepository import com.thomaskioko.tvmaniac.seasondetails.api.SeasonDetailsRepository import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -12,7 +9,7 @@ import me.tatarka.inject.annotations.Inject @OptIn(ExperimentalCoroutinesApi::class) @Inject -class SeasonDetailsStateMachine constructor( +class SeasonDetailsStateMachine( @Assisted private val traktId: Long, private val seasonDetailsRepository: SeasonDetailsRepository, private val episodeImageRepository: EpisodeImageRepository, @@ -22,15 +19,24 @@ class SeasonDetailsStateMachine constructor( spec { inState { onEnter { state -> - fetchSeasonDetails(state) + val seasonList = seasonDetailsRepository.fetchSeasonDetails(traktId) + + state.override { + SeasonDetailsLoaded( + showTitle = seasonList.getTitle(), + seasonDetailsList = seasonList.toSeasonWithEpisodes(), + ) + } } } inState { - collectWhileInState(seasonDetailsRepository.observeSeasonDetails(traktId)) { result, state -> + collectWhileInState(seasonDetailsRepository.observeSeasonDetailsStream(traktId)) { result, state -> result.fold( { - state.override { LoadingError(it.errorMessage) } + state.mutate { + copy(errorMessage = it.errorMessage) + } }, { state.mutate { @@ -40,49 +46,17 @@ class SeasonDetailsStateMachine constructor( ) } - collectWhileInStateEffect(episodeImageRepository.updateEpisodeImage()) { _, _ -> + collectWhileInStateEffect(episodeImageRepository.updateEpisodeImage(traktId)) { _, _ -> /** No need to do anything. Just trigger artwork download. **/ } } inState { - on { action, state -> - var nextState: SeasonDetailsState = state.snapshot - - seasonDetailsRepository.observeSeasonDetailsStream(traktId = action.showId) - .collect { result -> - nextState = when (result) { - is Either.Left -> LoadingError(result.error.errorMessage) - is Either.Right -> SeasonDetailsLoaded( - showTitle = result.getTitle(), - seasonDetailsList = result.toSeasonWithEpisodes(), - ) - } - } + on { _, state -> state.override { Loading } } } } } - - private suspend fun fetchSeasonDetails(state: State): ChangedState { - var nextState: SeasonDetailsState = Loading - - seasonDetailsRepository.observeSeasonDetailsStream(traktId) - .collect { result -> - - nextState = result.fold( - { LoadingError(it.errorMessage) }, - { - SeasonDetailsLoaded( - showTitle = it.getTitle(), - seasonDetailsList = it.toSeasonWithEpisodes(), - ) - }, - ) - } - - return state.override { nextState } - } } diff --git a/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/model/SeasonDetails.kt b/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/model/SeasonDetails.kt index b46347ed2..312eeeb06 100644 --- a/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/model/SeasonDetails.kt +++ b/presentation/seasondetails/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/seasondetails/model/SeasonDetails.kt @@ -1,9 +1,11 @@ package com.thomaskioko.tvmaniac.presentation.seasondetails.model +import kotlinx.collections.immutable.ImmutableList + data class SeasonDetails( val seasonId: Long, val seasonName: String, val episodeCount: Long, val watchProgress: Float, - val episodes: List, + val episodes: ImmutableList, ) diff --git a/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/MockData.kt b/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/MockData.kt index 261f3f860..73f19a495 100644 --- a/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/MockData.kt +++ b/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/MockData.kt @@ -3,8 +3,9 @@ package com.thomaskioko.tvmaniac.data.seasondetails import com.thomaskioko.tvmaniac.presentation.seasondetails.SeasonDetailsLoaded import com.thomaskioko.tvmaniac.presentation.seasondetails.model.Episode import com.thomaskioko.tvmaniac.presentation.seasondetails.model.SeasonDetails +import kotlinx.collections.immutable.persistentListOf -val episodes = listOf( +val episodes = persistentListOf( Episode( id = 12345, seasonId = 12343, @@ -19,7 +20,7 @@ val episodes = listOf( ), ) -val seasonDetailsList = listOf( +val seasonDetailsList = persistentListOf( SeasonDetails( seasonId = 12343, seasonName = "Season 01", diff --git a/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/SeasonDetailsStateMachineTest.kt b/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/SeasonDetailsStateMachineTest.kt index bdd7d2c1e..01f98cc37 100644 --- a/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/SeasonDetailsStateMachineTest.kt +++ b/presentation/seasondetails/src/commonTest/kotlin/com/thomaskioko/tvmaniac/data/seasondetails/SeasonDetailsStateMachineTest.kt @@ -1,20 +1,17 @@ package com.thomaskioko.tvmaniac.data.seasondetails import app.cash.turbine.test -import com.thomaskioko.tvmaniac.core.networkutil.DefaultError -import com.thomaskioko.tvmaniac.core.networkutil.Either import com.thomaskioko.tvmaniac.episodes.testing.FakeEpisodeImageRepository import com.thomaskioko.tvmaniac.presentation.seasondetails.Loading -import com.thomaskioko.tvmaniac.presentation.seasondetails.LoadingError import com.thomaskioko.tvmaniac.presentation.seasondetails.SeasonDetailsStateMachine import com.thomaskioko.tvmaniac.seasondetails.testing.FakeSeasonDetailsRepository -import com.thomaskioko.tvmaniac.seasondetails.testing.seasonDetails +import com.thomaskioko.tvmaniac.seasondetails.testing.SeasonWithEpisodeList +import com.thomaskioko.tvmaniac.util.model.DefaultError +import com.thomaskioko.tvmaniac.util.model.Either import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runTest -import kotlin.test.Ignore import kotlin.test.Test -@Ignore // TODO:: Fix test class SeasonDetailsStateMachineTest { private val seasonDetailsRepository = FakeSeasonDetailsRepository() @@ -28,7 +25,7 @@ class SeasonDetailsStateMachineTest { @Test fun onLoadSeasonDetails_correct_state_is_emitted() = runTest { stateMachine.state.test { - seasonDetailsRepository.setSeasonDetails(Either.Right(seasonDetails)) + seasonDetailsRepository.setCachedResults(SeasonWithEpisodeList) awaitItem() shouldBe Loading awaitItem() shouldBe seasonDetailsLoaded @@ -39,10 +36,13 @@ class SeasonDetailsStateMachineTest { fun onLoadSeasonDetails_andErrorOccurs_correctStateIsEmitted() = runTest { stateMachine.state.test { val errorMessage = "Something went wrong" - seasonDetailsRepository.setSeasonDetails(Either.Left(DefaultError(errorMessage))) + seasonDetailsRepository.setCachedResults(SeasonWithEpisodeList) + seasonDetailsRepository.setSeasonsResult(Either.Left(DefaultError(errorMessage))) awaitItem() shouldBe Loading - awaitItem() shouldBe LoadingError(errorMessage) + awaitItem() shouldBe seasonDetailsLoaded + awaitItem() shouldBe seasonDetailsLoaded + .copy(errorMessage = errorMessage) } } } diff --git a/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsStateMachine.kt b/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsStateMachine.kt index 362897de8..834774869 100644 --- a/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsStateMachine.kt +++ b/presentation/settings/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsStateMachine.kt @@ -5,12 +5,11 @@ 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.Either import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import me.tatarka.inject.annotations.Inject -import org.mobilenativefoundation.store.store5.StoreReadResponse -@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class) @Inject class SettingsStateMachine( private val datastoreRepository: DatastoreRepository, @@ -109,39 +108,24 @@ class SettingsStateMachine( } collectWhileInState(profileRepository.observeProfile("me")) { response, state -> - when (response) { - is StoreReadResponse.NoNewData -> state.noChange() - is StoreReadResponse.Loading -> state.mutate { + is Either.Left -> state.mutate { copy( - isLoading = true, + isLoading = false, + errorMessage = response.error.errorMessage, ) } - is StoreReadResponse.Data -> state.mutate { + is Either.Right -> state.mutate { copy( isLoading = false, userInfo = UserInfo( - slug = response.requireData().slug, - userName = response.requireData().user_name, - fullName = response.requireData().full_name, - userPicUrl = response.requireData().profile_picture, + slug = response.data.slug, + userName = response.data.user_name, + fullName = response.data.full_name, + userPicUrl = response.data.profile_picture, ), ) } - - is StoreReadResponse.Error.Exception -> state.mutate { - copy( - isLoading = false, - errorMessage = response.error.message, - ) - } - - is StoreReadResponse.Error.Message -> state.mutate { - copy( - isLoading = false, - errorMessage = response.message, - ) - } } } diff --git a/presentation/settings/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsStateMachineTest.kt b/presentation/settings/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsStateMachineTest.kt index cfd44776e..61f7f230e 100644 --- a/presentation/settings/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsStateMachineTest.kt +++ b/presentation/settings/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/settings/SettingsStateMachineTest.kt @@ -8,10 +8,10 @@ import com.thomaskioko.tvmaniac.trakt.profile.testing.FakeProfileRepository import com.thomaskioko.tvmaniac.trakt.profile.testing.user import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthState import com.thomaskioko.tvmaniac.traktauth.testing.FakeTraktAuthRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.ServerError import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runTest -import org.mobilenativefoundation.store.store5.StoreReadResponse -import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin import kotlin.test.Test class SettingsStateMachineTest { @@ -115,12 +115,7 @@ class SettingsStateMachineTest { traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) datastoreRepository.setAuthState(authenticatedAuthState) - profileRepository.setUserData( - StoreReadResponse.Data( - value = user, - origin = StoreReadResponseOrigin.Cache, - ), - ) + profileRepository.setUserData(Either.Right(user)) awaitItem() shouldBe LoggedInContent.DEFAULT_STATE awaitItem() shouldBe LoggedInContent.DEFAULT_STATE @@ -157,12 +152,7 @@ class SettingsStateMachineTest { traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) datastoreRepository.setAuthState(authenticatedAuthState) - profileRepository.setUserData( - StoreReadResponse.Error.Exception( - error = Throwable(errorMessage), - origin = StoreReadResponseOrigin.Cache, - ), - ) + profileRepository.setUserData(Either.Left(ServerError(errorMessage))) awaitItem() shouldBe LoggedInContent.DEFAULT_STATE awaitItem() shouldBe LoggedInContent.DEFAULT_STATE @@ -179,12 +169,7 @@ class SettingsStateMachineTest { traktAuthRepository.setAuthState(TraktAuthState.LOGGED_IN) datastoreRepository.setAuthState(authenticatedAuthState) - profileRepository.setUserData( - StoreReadResponse.Data( - value = user, - origin = StoreReadResponseOrigin.Cache, - ), - ) + profileRepository.setUserData(Either.Right(user)) awaitItem() shouldBe LoggedInContent.DEFAULT_STATE awaitItem() shouldBe LoggedInContent.DEFAULT_STATE diff --git a/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailMapper.kt b/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailMapper.kt index 2f0ca1653..050249d4d 100644 --- a/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailMapper.kt +++ b/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailMapper.kt @@ -6,11 +6,11 @@ import com.thomaskioko.tvmaniac.core.db.Trailers import com.thomaskioko.tvmaniac.presentation.showdetails.model.Season import com.thomaskioko.tvmaniac.presentation.showdetails.model.Show import com.thomaskioko.tvmaniac.presentation.showdetails.model.Trailer -import com.thomaskioko.tvmaniac.core.db.Seasons as SeasonCache +import com.thomaskioko.tvmaniac.core.db.SeasonsByShowId as SeasonCache fun List?.toSimilarShowList(): List = this?.map { Show( - traktId = it.trakt_id, + traktId = it.id.id, tmdbId = it.tmdb_id, title = it.title, overview = it.overview, @@ -27,7 +27,7 @@ fun List?.toSimilarShowList(): List = this?.map { fun ShowById?.toTvShow(): Show = this?.let { Show( - traktId = it.trakt_id, + traktId = it.id.id, tmdbId = it.tmdb_id, title = it.title, overview = it.overview, @@ -39,22 +39,21 @@ fun ShowById?.toTvShow(): Show = this?.let { genres = it.genres, year = it.year, status = it.status, - isFollowed = it.id != null && it.id == it.trakt_id, - // TODO:: Get season count + isFollowed = it.in_watchlist == 1L, ) } ?: Show.EMPTY_SHOW fun List?.toSeasonsList(): List = this?.map { Season( - seasonId = it.id, - tvShowId = it.show_trakt_id, - name = it.name, + seasonId = it.season_id.id, + tvShowId = it.show_id.id, + name = it.season_title, ) } ?: emptyList() fun List?.toTrailerList(): List = this?.map { Trailer( - showId = it.trakt_id, + showId = it.show_id.id, key = it.key, name = it.name, youtubeThumbnailUrl = "https://i.ytimg.com/vi/${it.key}/hqdefault.jpg", diff --git a/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsStateMachine.kt b/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsStateMachine.kt index 58c5b7657..71efb4e35 100644 --- a/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsStateMachine.kt +++ b/presentation/show-details/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsStateMachine.kt @@ -3,7 +3,8 @@ package com.thomaskioko.tvmaniac.presentation.showdetails import com.freeletics.flowredux.dsl.ChangedState import com.freeletics.flowredux.dsl.FlowReduxStateMachine import com.freeletics.flowredux.dsl.State -import com.thomaskioko.tvmaniac.core.db.Seasons +import com.thomaskioko.tvmaniac.core.db.SeasonsByShowId +import com.thomaskioko.tvmaniac.core.db.ShowById import com.thomaskioko.tvmaniac.core.db.SimilarShows import com.thomaskioko.tvmaniac.core.db.Trailers import com.thomaskioko.tvmaniac.data.trailers.implementation.TrailerRepository @@ -12,14 +13,15 @@ import com.thomaskioko.tvmaniac.seasons.api.SeasonsRepository import com.thomaskioko.tvmaniac.shows.api.DiscoverRepository import com.thomaskioko.tvmaniac.shows.api.WatchlistRepository import com.thomaskioko.tvmaniac.similar.api.SimilarShowsRepository +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.Failure import kotlinx.coroutines.ExperimentalCoroutinesApi import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject -import org.mobilenativefoundation.store.store5.StoreReadResponse @OptIn(ExperimentalCoroutinesApi::class) @Inject -class ShowDetailsStateMachine constructor( +class ShowDetailsStateMachine( @Assisted private val traktShowId: Long, private val discoverRepository: DiscoverRepository, private val similarShowsRepository: SimilarShowsRepository, @@ -39,38 +41,17 @@ class ShowDetailsStateMachine constructor( fetchShowDetails(state) } - collectWhileInState(discoverRepository.observeShow(traktShowId)) { response, state -> - when (response) { - is StoreReadResponse.NoNewData -> state.noChange() - is StoreReadResponse.Loading -> state.mutate { - copy(isLoading = true) - } - - is StoreReadResponse.Data -> state.mutate { - copy( - isLoading = false, - show = response.requireData().toTvShow(), - ) - } - - is StoreReadResponse.Error.Exception -> - state.mutate { - copy( - isLoading = false, - errorMessage = response.errorMessageOrNull(), - ) - } - - is StoreReadResponse.Error.Message -> state.mutate { - copy( - isLoading = false, - errorMessage = response.message, - ) - } + untilIdentityChanges({ state -> state.show.traktId }) { + collectWhileInState(discoverRepository.observeShow(traktShowId)) { result, state -> + updateShowDetails(result, state) } } - collectWhileInState(seasonsRepository.observeSeasonsStoreResponse(traktShowId)) { result, state -> + collectWhileInState(discoverRepository.observeShow(traktShowId)) { response, state -> + updateShowDetails(response, state) + } + + collectWhileInState(seasonsRepository.observeSeasonsByShowId(traktShowId)) { result, state -> updateSeasonDetailsState(result, state) } @@ -119,136 +100,90 @@ class ShowDetailsStateMachine constructor( } } - private fun updateSimilarShowsState( - response: StoreReadResponse>, + private fun updateShowDetails( + response: Either, state: State, ) = when (response) { - is StoreReadResponse.NoNewData -> state.noChange() - is StoreReadResponse.Loading -> { - state.mutate { - copy( - similarShowsContent = similarShowsContent - .copy(isLoading = true), - ) - } - } - - is StoreReadResponse.Data -> { - state.mutate { - copy( - similarShowsContent = similarShowsContent.copy( - isLoading = false, - similarShows = response.requireData().toSimilarShowList(), - ), - ) - } - } - - is StoreReadResponse.Error.Exception -> { - state.mutate { - copy( - similarShowsContent = similarShowsContent.copy( - errorMessage = response.error.message, - ), - ) - } + is Either.Left -> state.mutate { + copy( + isLoading = false, + errorMessage = response.error.errorMessage, + ) } - is StoreReadResponse.Error.Message -> { - state.mutate { - copy( - similarShowsContent = similarShowsContent.copy( - errorMessage = response.message, - ), - ) - } + is Either.Right -> state.mutate { + copy( + isLoading = false, + show = response.data.toTvShow(), + ) } } - private fun updateTrailerState( - response: StoreReadResponse>, + private fun updateSimilarShowsState( + response: Either>, state: State, ) = when (response) { - is StoreReadResponse.NoNewData -> state.noChange() - is StoreReadResponse.Loading -> state.mutate { + is Either.Left -> state.mutate { copy( - trailersContent = trailersContent.copy( - isLoading = true, + similarShowsContent = similarShowsContent.copy( + errorMessage = response.error.errorMessage, ), ) } - is StoreReadResponse.Data -> { - state.mutate { - copy( - trailersContent = trailersContent.copy( - isLoading = false, - trailersList = response.requireData().toTrailerList(), - ), - ) - } - } - - is StoreReadResponse.Error.Exception -> { - state.mutate { - copy( - trailersContent = trailersContent.copy( - errorMessage = response.error.message, - ), - ) - } - } - - is StoreReadResponse.Error.Message -> { - state.mutate { - copy(trailersContent = trailersContent.copy(errorMessage = response.message)) - } + is Either.Right -> state.mutate { + copy( + similarShowsContent = similarShowsContent.copy( + isLoading = false, + similarShows = response.data.toSimilarShowList(), + ), + ) } } - private fun updateSeasonDetailsState( - response: StoreReadResponse>, + private fun updateTrailerState( + response: Either>, state: State, ) = when (response) { - is StoreReadResponse.NoNewData -> state.noChange() - is StoreReadResponse.Loading -> { + is Either.Left -> { state.mutate { - copy( - seasonsContent = seasonsContent.copy( - isLoading = true, - ), - ) + copy(trailersContent = trailersContent.copy(errorMessage = response.error.errorMessage)) } } - is StoreReadResponse.Data -> { + is Either.Right -> { state.mutate { copy( - seasonsContent = seasonsContent.copy( + trailersContent = trailersContent.copy( isLoading = false, - seasonsList = response.requireData().toSeasonsList(), + trailersList = response.data.toTrailerList(), ), ) } } + } - is StoreReadResponse.Error.Exception -> { + private fun updateSeasonDetailsState( + response: Either>, + state: State, + ) = when (response) { + is Either.Left -> { state.mutate { copy( seasonsContent = seasonsContent.copy( isLoading = true, - errorMessage = response.error.message, + errorMessage = response.error.errorMessage, ), ) } } - is StoreReadResponse.Error.Message -> { + is Either.Right -> { state.mutate { copy( seasonsContent = seasonsContent.copy( - isLoading = true, - errorMessage = response.message, + isLoading = false, + seasonsList = response.data.toSeasonsList(), ), ) } @@ -258,7 +193,7 @@ class ShowDetailsStateMachine constructor( private suspend fun fetchShowDetails(state: State): ChangedState { val show = discoverRepository.getShowById(traktShowId) val similar = similarShowsRepository.fetchSimilarShows(traktShowId) - val season = seasonsRepository.getSeasons(traktShowId) + val season = seasonsRepository.fetchSeasonsByShowId(traktShowId) val trailers = trailerRepository.fetchTrailersByShowId(traktShowId) return state.mutate { diff --git a/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/MockData.kt b/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/MockData.kt index 3bfc907d3..45434eabd 100644 --- a/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/MockData.kt +++ b/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/MockData.kt @@ -1,11 +1,12 @@ package com.thomaskioko.tvmaniac.presentation.showdetails +import com.thomaskioko.tvmaniac.core.db.SeasonsByShowId import com.thomaskioko.tvmaniac.core.db.ShowById import com.thomaskioko.tvmaniac.core.db.SimilarShows +import com.thomaskioko.tvmaniac.db.Id import com.thomaskioko.tvmaniac.presentation.showdetails.model.Season import com.thomaskioko.tvmaniac.presentation.showdetails.model.Show import com.thomaskioko.tvmaniac.presentation.showdetails.model.Trailer -import com.thomaskioko.tvmaniac.core.db.Seasons as SeasonCache val show = Show( traktId = 84958, @@ -98,7 +99,7 @@ val similarShowLoaded = ShowDetailsLoaded.SimilarShowsContent( ) val selectedShow = ShowById( - trakt_id = 84958, + id = Id(84958), tmdb_id = 849583, title = "Loki", overview = "After stealing the Tesseract during the events of “Avengers: Endgame,” " + @@ -117,15 +118,11 @@ val selectedShow = ShowById( poster_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", backdrop_url = "/kEl2t3OhXc3Zb9FBh1AuYzRTgZp.jpg", aired_episodes = 12, - trakt_id_ = 1234, - id = 12345, - created_at = null, - synced = false, - tmdb_id_ = 1232, + in_watchlist = 0, ) val similarShowResult = listOf( SimilarShows( - trakt_id = 184958, + id = Id(184958), tmdb_id = 284958, title = "Loki", overview = "After stealing the Tesseract during the events of “Avengers: Endgame,” " + @@ -147,17 +144,10 @@ val similarShowResult = listOf( ) val seasons = listOf( - SeasonCache( - id = 84958, - show_trakt_id = 114355, - name = "Season 1", - episode_count = 10, + SeasonsByShowId( + season_id = Id(84958), + show_id = Id(114355), + season_title = "Season 1", season_number = 1, - overview = "After stealing the Tesseract during the events of “Avengers: Endgame,” " + - "an alternate version of Loki is brought to the mysterious Time Variance " + - "Authority, a bureaucratic organization that exists outside of time and " + - "space and monitors the timeline. They give Loki a choice: face being " + - "erased from existence due to being a “time variant”or help fix " + - "the timeline and stop a greater threat.", ), ) diff --git a/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsStateMachineTest.kt b/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsStateMachineTest.kt index 1a819bc67..57034196e 100644 --- a/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsStateMachineTest.kt +++ b/presentation/show-details/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/showdetails/ShowDetailsStateMachineTest.kt @@ -1,19 +1,21 @@ package com.thomaskioko.tvmaniac.presentation.showdetails import app.cash.turbine.test +import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsLoaded.Companion.EMPTY_DETAIL_STATE import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsLoaded.SeasonsContent.Companion.EMPTY_SEASONS import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsLoaded.SimilarShowsContent.Companion.EMPTY_SIMILAR_SHOWS import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsLoaded.TrailersContent.Companion.EMPTY_TRAILERS import com.thomaskioko.tvmaniac.seasons.testing.FakeSeasonsRepository import com.thomaskioko.tvmaniac.shows.testing.FakeDiscoverRepository +import com.thomaskioko.tvmaniac.shows.testing.selectedShow import com.thomaskioko.tvmaniac.similar.testing.FakeSimilarShowsRepository import com.thomaskioko.tvmaniac.trailers.testing.FakeTrailerRepository import com.thomaskioko.tvmaniac.trailers.testing.trailers +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.ServerError import com.thomaskioko.tvmaniac.watchlist.testing.FakeWatchlistRepository import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runTest -import org.mobilenativefoundation.store.store5.StoreReadResponse -import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin import kotlin.test.Ignore import kotlin.test.Test @@ -22,13 +24,13 @@ internal class ShowDetailsStateMachineTest { private val seasonsRepository = FakeSeasonsRepository() private val trailerRepository = FakeTrailerRepository() - private val traktRepository = FakeDiscoverRepository() + private val discoverRepository = FakeDiscoverRepository() private val similarShowsRepository = FakeSimilarShowsRepository() private val watchlistRepository = FakeWatchlistRepository() private val stateMachine = ShowDetailsStateMachine( traktShowId = 84958, - discoverRepository = traktRepository, + discoverRepository = discoverRepository, trailerRepository = trailerRepository, seasonsRepository = seasonsRepository, similarShowsRepository = similarShowsRepository, @@ -38,43 +40,25 @@ internal class ShowDetailsStateMachineTest { @Test fun initial_state_emits_expected_result() = runTest { stateMachine.state.test { - stateMachine.dispatch(LoadShowDetails(84958)) + discoverRepository.setShowById(selectedShow) - awaitItem() shouldBe ShowDetailsLoaded.EMPTY_DETAIL_STATE + awaitItem() shouldBe EMPTY_DETAIL_STATE.copy( + show = show, + ) } } @Test fun loadingData_state_emits_expected_result() = runTest { stateMachine.state.test { - traktRepository.setShowResult( - StoreReadResponse.Data( - value = selectedShow, - origin = StoreReadResponseOrigin.Cache, - ), - ) - seasonsRepository.setSeasonsResult( - StoreReadResponse.Data( - value = seasons, - origin = StoreReadResponseOrigin.Cache, - ), - ) - similarShowsRepository.setSimilarShowsResult( - StoreReadResponse.Data( - value = similarShowResult, - origin = StoreReadResponseOrigin.Cache, - ), - ) - trailerRepository.setTrailerResult( - StoreReadResponse.Data( - value = trailers, - origin = StoreReadResponseOrigin.Cache, - ), - ) + discoverRepository.setShowResult(Either.Right(selectedShow)) + seasonsRepository.setSeasonsResult(Either.Right(seasons)) + similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) + trailerRepository.setTrailerResult(Either.Right(trailers)) stateMachine.dispatch(LoadShowDetails(84958)) - awaitItem() shouldBe ShowDetailsLoaded.EMPTY_DETAIL_STATE + awaitItem() shouldBe EMPTY_DETAIL_STATE awaitItem() shouldBe showDetailsLoaded awaitItem() shouldBe showDetailsLoaded.copy( seasonsContent = seasonsShowDetailsLoaded, @@ -95,34 +79,14 @@ internal class ShowDetailsStateMachineTest { fun error_loading_similarShows_emits_expected_result() = runTest { stateMachine.state.test { val errorMessage = "Something went wrong" - traktRepository.setShowResult( - StoreReadResponse.Data( - value = selectedShow, - origin = StoreReadResponseOrigin.Cache, - ), - ) - seasonsRepository.setSeasonsResult( - StoreReadResponse.Data( - value = seasons, - origin = StoreReadResponseOrigin.Cache, - ), - ) - trailerRepository.setTrailerResult( - StoreReadResponse.Data( - value = trailers, - origin = StoreReadResponseOrigin.Cache, - ), - ) - similarShowsRepository.setSimilarShowsResult( - StoreReadResponse.Error.Message( - message = errorMessage, - origin = StoreReadResponseOrigin.Cache, - ), - ) + discoverRepository.setShowResult(Either.Right(selectedShow)) + seasonsRepository.setSeasonsResult(Either.Right(seasons)) + trailerRepository.setTrailerResult(Either.Right(trailers)) + similarShowsRepository.setSimilarShowsResult(Either.Left(ServerError(errorMessage))) stateMachine.dispatch(LoadShowDetails(84958)) - awaitItem() shouldBe ShowDetailsLoaded.EMPTY_DETAIL_STATE + awaitItem() shouldBe EMPTY_DETAIL_STATE awaitItem() shouldBe showDetailsLoaded awaitItem() shouldBe showDetailsLoaded.copy( seasonsContent = seasonsShowDetailsLoaded, @@ -145,34 +109,14 @@ internal class ShowDetailsStateMachineTest { fun error_loading_trailers_emits_expected_result() = runTest { stateMachine.state.test { val errorMessage = "Something went wrong" - traktRepository.setShowResult( - StoreReadResponse.Data( - value = selectedShow, - origin = StoreReadResponseOrigin.Cache, - ), - ) - seasonsRepository.setSeasonsResult( - StoreReadResponse.Data( - value = seasons, - origin = StoreReadResponseOrigin.Cache, - ), - ) - similarShowsRepository.setSimilarShowsResult( - StoreReadResponse.Data( - value = similarShowResult, - origin = StoreReadResponseOrigin.Cache, - ), - ) - trailerRepository.setTrailerResult( - StoreReadResponse.Error.Message( - message = errorMessage, - origin = StoreReadResponseOrigin.Cache, - ), - ) + discoverRepository.setShowResult(Either.Right(selectedShow)) + seasonsRepository.setSeasonsResult(Either.Right(seasons)) + similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) + trailerRepository.setTrailerResult(Either.Left(ServerError(errorMessage))) stateMachine.dispatch(LoadShowDetails(84958)) - awaitItem() shouldBe ShowDetailsLoaded.EMPTY_DETAIL_STATE + awaitItem() shouldBe EMPTY_DETAIL_STATE awaitItem() shouldBe showDetailsLoaded awaitItem() shouldBe showDetailsLoaded.copy( seasonsContent = seasonsShowDetailsLoaded, @@ -197,32 +141,12 @@ internal class ShowDetailsStateMachineTest { fun error_loading_seasons_emits_expected_result() = runTest { stateMachine.state.test { val errorMessage = "Something went wrong" - traktRepository.setShowResult( - StoreReadResponse.Data( - value = selectedShow, - origin = StoreReadResponseOrigin.Cache, - ), - ) - trailerRepository.setTrailerResult( - StoreReadResponse.Data( - value = trailers, - origin = StoreReadResponseOrigin.Cache, - ), - ) - similarShowsRepository.setSimilarShowsResult( - StoreReadResponse.Data( - value = similarShowResult, - origin = StoreReadResponseOrigin.Cache, - ), - ) - seasonsRepository.setSeasonsResult( - StoreReadResponse.Error.Message( - message = errorMessage, - origin = StoreReadResponseOrigin.Cache, - ), - ) + discoverRepository.setShowResult(Either.Right(selectedShow)) + similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) + trailerRepository.setTrailerResult(Either.Right(trailers)) + seasonsRepository.setSeasonWithEpisodes(Either.Left(ServerError(errorMessage))) - awaitItem() shouldBe ShowDetailsLoaded.EMPTY_DETAIL_STATE + awaitItem() shouldBe EMPTY_DETAIL_STATE awaitItem() shouldBe showDetailsLoaded awaitItem() shouldBe showDetailsLoaded.copy( seasonsContent = EMPTY_SEASONS.copy( @@ -249,35 +173,14 @@ internal class ShowDetailsStateMachineTest { fun error_state_emits_expected_result() = runTest { stateMachine.state.test { val errorMessage = "Something went wrong" - traktRepository.setShowResult( - StoreReadResponse.Error.Message( - message = errorMessage, - origin = StoreReadResponseOrigin.Cache, - ), - ) - seasonsRepository.setSeasonsResult( - StoreReadResponse.Error.Message( - message = errorMessage, - origin = StoreReadResponseOrigin.Cache, - ), - ) - similarShowsRepository.setSimilarShowsResult( - StoreReadResponse.Data( - value = similarShowResult, - origin = StoreReadResponseOrigin.Cache, - ), - ) - trailerRepository.setTrailerResult( - StoreReadResponse.Data( - value = trailers, - origin = StoreReadResponseOrigin.Cache, - ), - ) - + discoverRepository.setShowResult(Either.Left(ServerError(errorMessage))) + similarShowsRepository.setSimilarShowsResult(Either.Right(similarShowResult)) + trailerRepository.setTrailerResult(Either.Right(trailers)) + seasonsRepository.setSeasonsResult(Either.Right(seasons)) stateMachine.dispatch(LoadShowDetails(84958)) - awaitItem() shouldBe ShowDetailsLoaded.EMPTY_DETAIL_STATE - awaitItem() shouldBe ShowDetailsLoaded.EMPTY_DETAIL_STATE.copy( + awaitItem() shouldBe EMPTY_DETAIL_STATE + awaitItem() shouldBe EMPTY_DETAIL_STATE.copy( errorMessage = errorMessage, ) } diff --git a/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/Mapper.kt b/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/Mapper.kt index 945541190..8caf14feb 100644 --- a/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/Mapper.kt +++ b/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/Mapper.kt @@ -3,13 +3,13 @@ package com.thomaskioko.tvmaniac.presentation.trailers import com.thomaskioko.tvmaniac.core.db.Trailers import com.thomaskioko.tvmaniac.presentation.trailers.model.Trailer -internal fun List?.toTrailerList(): List { - return this?.map { +internal fun List.toTrailerList(): List { + return map { Trailer( - showId = it.trakt_id, + showId = it.show_id.id, key = it.key, name = it.name, youtubeThumbnailUrl = "https://i.ytimg.com/vi/${it.key}/hqdefault.jpg", ) - } ?: emptyList() + } } diff --git a/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersState.kt b/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersState.kt index 8d6e13144..fdbf64a68 100644 --- a/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersState.kt +++ b/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersState.kt @@ -11,4 +11,4 @@ data class TrailersContent( val trailersList: List = emptyList(), ) : TrailersState -data class TrailerError(val errorMessage: String) : TrailersState +data class TrailerError(val errorMessage: String?) : TrailersState diff --git a/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersStateMachine.kt b/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersStateMachine.kt index 70f741191..4d8250496 100644 --- a/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersStateMachine.kt +++ b/presentation/trailers/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailersStateMachine.kt @@ -1,16 +1,13 @@ package com.thomaskioko.tvmaniac.presentation.trailers -import com.freeletics.flowredux.dsl.ChangedState import com.freeletics.flowredux.dsl.FlowReduxStateMachine -import com.freeletics.flowredux.dsl.State import com.thomaskioko.tvmaniac.data.trailers.implementation.TrailerRepository +import com.thomaskioko.tvmaniac.util.model.Either import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject -import org.mobilenativefoundation.store.store5.StoreReadResponse -@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class) @Inject class TrailersStateMachine( @Assisted private val traktShowId: Long, @@ -31,6 +28,25 @@ class TrailersStateMachine( } } + untilIdentityChanges({ state -> state }) { + collectWhileInState(repository.observeTrailersStoreResponse(traktShowId)) { response, state -> + when (response) { + is Either.Left -> { + state.override { TrailerError(response.error.errorMessage) } + } + + is Either.Right -> { + state.override { + TrailersContent( + selectedVideoKey = response.data.toTrailerList() + .firstOrNull()?.key, + trailersList = response.data.toTrailerList(), + ) + } + } + } + } + } on { action, state -> state.override { TrailerError(action.errorMessage) } } @@ -45,64 +61,33 @@ class TrailersStateMachine( collectWhileInState(repository.observeTrailersStoreResponse(traktShowId)) { response, state -> when (response) { - is StoreReadResponse.Loading -> state.override { LoadingTrailers } - is StoreReadResponse.NoNewData -> state.noChange() - is StoreReadResponse.Data -> { + is Either.Left -> { + state.override { TrailerError(response.error.errorMessage) } + } + + is Either.Right -> { state.mutate { copy( - selectedVideoKey = response.requireData().toTrailerList() + selectedVideoKey = response.data.toTrailerList() .firstOrNull()?.key, - trailersList = response.requireData().toTrailerList(), + trailersList = response.data.toTrailerList(), ) } } - - is StoreReadResponse.Error.Exception -> { - state.override { TrailerError("") } - } - - is StoreReadResponse.Error.Message -> { - state.override { TrailerError(response.message) } - } } } - } - - inState { on { _, state -> - reloadTrailers(state) + state.override { LoadingTrailers } } } - } - } - - private suspend fun reloadTrailers(state: State): ChangedState { - var trailerState: ChangedState = state.override { LoadingTrailers } - repository.observeTrailersStoreResponse(traktShowId) - .collect { response -> - trailerState = when (response) { - is StoreReadResponse.Loading -> state.override { LoadingTrailers } - is StoreReadResponse.NoNewData -> state.noChange() - is StoreReadResponse.Data -> { - state.override { - TrailersContent( - selectedVideoKey = response.requireData().toTrailerList() - .firstOrNull()?.key, - trailersList = response.requireData().toTrailerList(), - ) - } - } - is StoreReadResponse.Error.Exception -> { - state.override { TrailerError("") } - } + inState { - is StoreReadResponse.Error.Message -> { - state.override { TrailerError(response.message) } - } + on { _, state -> + state.override { LoadingTrailers } } } - return trailerState + } } } diff --git a/presentation/trailers/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailerStateMachineTest.kt b/presentation/trailers/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailerStateMachineTest.kt index d3d478abe..c92141439 100644 --- a/presentation/trailers/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailerStateMachineTest.kt +++ b/presentation/trailers/src/commonTest/kotlin/com/thomaskioko/tvmaniac/presentation/trailers/TrailerStateMachineTest.kt @@ -4,14 +4,12 @@ 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 +import com.thomaskioko.tvmaniac.util.model.Either +import com.thomaskioko.tvmaniac.util.model.ServerError import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runTest -import org.mobilenativefoundation.store.store5.StoreReadResponse -import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin -import kotlin.test.Ignore import kotlin.test.Test -@Ignore internal class TrailerStateMachineTest { private val repository = FakeTrailerRepository() @@ -21,22 +19,51 @@ internal class TrailerStateMachineTest { ) @Test - fun reloadTrailers_emits_expected_result() = runTest { + fun `given result is success correct state is emitted`() = runTest { stateMachine.state.test { repository.setTrailerList(trailers) - repository.setTrailerResult( - StoreReadResponse.Error.Message( - message = "Something went wrong.", - origin = StoreReadResponseOrigin.Cache, + awaitItem() shouldBe LoadingTrailers + awaitItem() shouldBe TrailersContent( + selectedVideoKey = "Fd43V", + trailersList = listOf( + 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 { + stateMachine.state.test { + repository.setTrailerList(trailers) + + repository.setTrailerResult(Either.Left(ServerError("Something went wrong."))) awaitItem() shouldBe LoadingTrailers + awaitItem() shouldBe TrailersContent( + selectedVideoKey = "Fd43V", + trailersList = listOf( + Trailer( + showId = 84958, + key = "Fd43V", + name = "Some title", + youtubeThumbnailUrl = "https://i.ytimg.com/vi/Fd43V/hqdefault.jpg", + ), + ), + ) + awaitItem() shouldBe TrailerError("Something went wrong.") stateMachine.dispatch(ReloadTrailers) + repository.setTrailerResult(Either.Right(trailers)) + awaitItem() shouldBe LoadingTrailers awaitItem() shouldBe TrailersContent( selectedVideoKey = "Fd43V", diff --git a/presentation/watchlist/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/Mapper.kt b/presentation/watchlist/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/Mapper.kt index ac40e560b..82334b07f 100644 --- a/presentation/watchlist/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/Mapper.kt +++ b/presentation/watchlist/src/commonMain/kotlin/com/thomaskioko/tvmaniac/presentation/watchlist/Mapper.kt @@ -1,11 +1,11 @@ package com.thomaskioko.tvmaniac.presentation.watchlist -import com.thomaskioko.tvmaniac.core.db.SelectWatchlist +import com.thomaskioko.tvmaniac.core.db.WatchedShow -fun List?.entityToWatchlist(): List { +fun List?.entityToWatchlist(): List { return this?.map { WatchlistItem( - traktId = it.id, + traktId = it.show_id.id, tmdbId = it.tmdb_id, title = it.title, posterImageUrl = it.poster_url, diff --git a/presentation/watchlist/src/commonTest/kotlin/com/thomaskioko/tvmaniac/domain/watchlist/WatchlistStateMachineTest.kt b/presentation/watchlist/src/commonTest/kotlin/com/thomaskioko/tvmaniac/domain/watchlist/WatchlistStateMachineTest.kt index 8d12a4f30..9ed148f15 100644 --- a/presentation/watchlist/src/commonTest/kotlin/com/thomaskioko/tvmaniac/domain/watchlist/WatchlistStateMachineTest.kt +++ b/presentation/watchlist/src/commonTest/kotlin/com/thomaskioko/tvmaniac/domain/watchlist/WatchlistStateMachineTest.kt @@ -1,7 +1,6 @@ package com.thomaskioko.tvmaniac.domain.watchlist import app.cash.turbine.test -import com.thomaskioko.tvmaniac.core.networkutil.Either import com.thomaskioko.tvmaniac.presentation.watchlist.LoadingShows import com.thomaskioko.tvmaniac.presentation.watchlist.WatchlistContent import com.thomaskioko.tvmaniac.presentation.watchlist.WatchlistStateMachine @@ -18,7 +17,7 @@ class WatchlistStateMachineTest { @Test fun initial_state_emits_expected_result() = runTest { - repository.setFollowedResult(Either.Right(data = watchlistResult)) + repository.setFollowedResult(watchlistResult) stateMachine.state.test { awaitItem() shouldBe LoadingShows diff --git a/settings.gradle.kts b/settings.gradle.kts index cd9654362..dc403e59f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,7 +38,6 @@ include( ":android-features:watchlist", ":shared", ":core:util", - ":core:networkutil", ":core:database", ":core:datastore:api", ":core:datastore:implementation", diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 9a5ae459d..53fb65463 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -55,7 +55,7 @@ kotlin { implementation(projects.core.datastore.implementation) implementation(projects.data.episodeimages.api) implementation(projects.data.watchlist.api) - implementation(projects.core.networkutil) + implementation(projects.core.util) implementation(projects.data.showimages.api) implementation(projects.core.traktApi.api) implementation(projects.core.traktApi.implementation) diff --git a/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/base/ApplicationComponent.kt b/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/base/ApplicationComponent.kt index 9caa3c40f..2af826c9b 100644 --- a/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/base/ApplicationComponent.kt +++ b/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/base/ApplicationComponent.kt @@ -1,11 +1,9 @@ package com.thomaskioko.tvmaniac.shared.base import com.thomaskioko.trakt.service.implementation.inject.TraktComponent -import com.thomaskioko.trakt.service.implementation.inject.TraktPlatformComponent -import com.thomaskioko.tvmaniac.core.networkutil.inject.NetworkPlatformComponent import com.thomaskioko.tvmaniac.data.category.implementation.CategoryComponent import com.thomaskioko.tvmaniac.data.trailers.implementation.TrailerComponent -import com.thomaskioko.tvmaniac.datastore.implementation.DataStorePlatformComponent +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 @@ -24,7 +22,7 @@ import com.thomaskioko.tvmaniac.shared.base.wrappers.WatchlistStateMachineWrappe 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.TmdbPlatformComponent +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 @@ -36,7 +34,7 @@ import me.tatarka.inject.annotations.Component abstract class ApplicationComponent : CategoryComponent, DatabaseComponent, - DataStorePlatformComponent, + DataStoreComponent, EpisodeComponent, EpisodeImageComponent, ProfileComponent, @@ -47,14 +45,12 @@ abstract class ApplicationComponent : ShowImagesComponent, SimilarShowsComponent, StatsComponent, - TmdbPlatformComponent, + TmdbComponent, TraktComponent, - TraktPlatformComponent, TraktAuthenticationComponent, TrailerComponent, UtilPlatformComponent, - WatchlistComponent, - NetworkPlatformComponent { + WatchlistComponent { abstract val discoverStateMachine: DiscoverStateMachineWrapper abstract val seasonDetailsStateMachineWrapper: SeasonDetailsStateMachineWrapper diff --git a/tooling/plugins/build.gradle.kts b/tooling/plugins/build.gradle.kts index 83b1c14a7..2e6888ce5 100644 --- a/tooling/plugins/build.gradle.kts +++ b/tooling/plugins/build.gradle.kts @@ -14,8 +14,8 @@ java { } dependencies { - compileOnly(libs.android.gradle.tools) - compileOnly(libs.kotlin.gradle) + compileOnly(libs.android.gradlePlugin) + compileOnly(libs.kotlin.gradlePlugin) } gradlePlugin {