diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/TvEpisodeScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/TvEpisodeScreen.kt index 03c9a263..907d5dba 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/TvEpisodeScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/episode/TvEpisodeScreen.kt @@ -21,9 +21,9 @@ import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.core.data.image.JellyfinImageHelper -import hu.bbara.purefin.core.data.navigation.EpisodeDto -import hu.bbara.purefin.core.data.navigation.LocalNavigationManager -import hu.bbara.purefin.core.data.navigation.Route +import hu.bbara.purefin.feature.shared.navigation.EpisodeDto +import hu.bbara.purefin.tv.navigation.LocalNavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel import org.jellyfin.sdk.model.api.ImageType diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/TvMovieScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/TvMovieScreen.kt index b1cb8dcb..6422974f 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/TvMovieScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/movie/TvMovieScreen.kt @@ -21,7 +21,7 @@ import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl -import hu.bbara.purefin.core.data.navigation.MovieDto +import hu.bbara.purefin.feature.shared.navigation.MovieDto import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel diff --git a/app-tv/src/main/java/hu/bbara/purefin/app/content/series/TvSeriesScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/app/content/series/TvSeriesScreen.kt index fe20990e..5cedeb04 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/app/content/series/TvSeriesScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/app/content/series/TvSeriesScreen.kt @@ -22,11 +22,11 @@ import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl -import hu.bbara.purefin.core.data.navigation.SeriesDto +import hu.bbara.purefin.feature.shared.navigation.SeriesDto import hu.bbara.purefin.core.model.Season import hu.bbara.purefin.core.model.Series import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel -import org.jellyfin.sdk.model.UUID +import java.util.UUID @Composable fun TvSeriesScreen( diff --git a/app-tv/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt b/app-tv/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt index 9e3f4502..f79a9d9e 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt @@ -34,8 +34,8 @@ import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator import hu.bbara.purefin.common.ui.components.WatchStateIndicator import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.shared.home.PosterItem -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind +import java.util.UUID +import hu.bbara.purefin.core.model.MediaKind @Composable fun PosterCard( @@ -57,9 +57,9 @@ fun PosterCard( fun openItem(posterItem: PosterItem) { when (posterItem.type) { - BaseItemKind.MOVIE -> onMovieSelected(posterItem.id) - BaseItemKind.SERIES -> onSeriesSelected(posterItem.id) - BaseItemKind.EPISODE -> { + MediaKind.MOVIE -> onMovieSelected(posterItem.id) + MediaKind.SERIES -> onSeriesSelected(posterItem.id) + MediaKind.EPISODE -> { val ep = posterItem.episode!! onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id) } @@ -99,7 +99,7 @@ fun PosterCard( contentScale = ContentScale.Crop ) when (item.type) { - BaseItemKind.MOVIE -> { + MediaKind.MOVIE -> { val m = item.movie!! WatchStateIndicator( size = indicatorSize, @@ -109,7 +109,7 @@ fun PosterCard( started = (m.progress ?: 0.0) > 0 ) } - BaseItemKind.EPISODE -> { + MediaKind.EPISODE -> { val ep = item.episode!! WatchStateIndicator( size = indicatorSize, @@ -119,7 +119,7 @@ fun PosterCard( started = (ep.progress ?: 0.0) > 0 ) } - BaseItemKind.SERIES -> UnwatchedEpisodeIndicator( + MediaKind.SERIES -> UnwatchedEpisodeIndicator( size = indicatorSize, modifier = Modifier.align(Alignment.TopEnd) .padding(indicatorPadding), diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt index 140d4547..eb970418 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt @@ -39,13 +39,13 @@ import dagger.hilt.android.AndroidEntryPoint import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.core.data.client.JellyfinApiClient import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor -import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack -import hu.bbara.purefin.core.data.navigation.LocalNavigationManager -import hu.bbara.purefin.core.data.navigation.NavigationCommand -import hu.bbara.purefin.core.data.navigation.NavigationManager -import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.session.UserSessionRepository +import hu.bbara.purefin.feature.shared.navigation.NavigationCommand +import hu.bbara.purefin.feature.shared.navigation.NavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.login.ui.LoginScreen +import hu.bbara.purefin.tv.navigation.LocalNavigationBackStack +import hu.bbara.purefin.tv.navigation.LocalNavigationManager import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.backgroundDark import kotlinx.coroutines.launch diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/TvAppScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/TvAppScreen.kt index af78ebfe..576080bf 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/TvAppScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/TvAppScreen.kt @@ -21,7 +21,7 @@ import hu.bbara.purefin.tv.home.ui.TvDrawerDestination import hu.bbara.purefin.tv.home.ui.TvDrawerDestinationItem import hu.bbara.purefin.tv.home.ui.TvNavigationDrawer import hu.bbara.purefin.tv.library.ui.TvLibrariesOverviewScreen -import org.jellyfin.sdk.model.api.CollectionType +import hu.bbara.purefin.core.model.LibraryKind @Composable fun TvAppScreen( @@ -48,8 +48,8 @@ fun TvAppScreen( destination = TvDrawerDestination.LIBRARIES, label = "Libraries", icon = when { - libraries.any { it.type == CollectionType.MOVIES } -> Icons.Outlined.Movie - libraries.any { it.type == CollectionType.TVSHOWS } -> Icons.Outlined.Tv + libraries.any { it.type == LibraryKind.MOVIES } -> Icons.Outlined.Movie + libraries.any { it.type == LibraryKind.SERIES } -> Icons.Outlined.Tv else -> Icons.Outlined.Collections }, selected = selectedDestination == TvDrawerDestination.LIBRARIES diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/di/TvPlaybackModule.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/di/TvPlaybackModule.kt index b5269998..61bb5bc6 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/di/TvPlaybackModule.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/di/TvPlaybackModule.kt @@ -11,7 +11,7 @@ import hu.bbara.purefin.core.data.download.MediaDownloadController import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf -import org.jellyfin.sdk.model.UUID +import java.util.UUID import javax.inject.Singleton @Module diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/home/TvHomeScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/home/TvHomeScreen.kt index 2369b989..b55561c0 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/home/TvHomeScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/home/TvHomeScreen.kt @@ -16,7 +16,7 @@ import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.tv.home.ui.TvFocusedItemHero import hu.bbara.purefin.tv.home.ui.TvHomeContent import hu.bbara.purefin.tv.home.ui.rememberTvHomeHeroState -import org.jellyfin.sdk.model.UUID +import java.util.UUID private const val TvHomeHeroHeightFraction = 0.32f private val TvHomeMinHeroHeight = 160.dp diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvFocusedHeroModel.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvFocusedHeroModel.kt index 014e8db0..96b04492 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvFocusedHeroModel.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvFocusedHeroModel.kt @@ -8,8 +8,8 @@ import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.PosterItem -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind +import java.util.UUID +import hu.bbara.purefin.core.model.MediaKind import org.jellyfin.sdk.model.api.ImageType import kotlin.math.roundToInt @@ -26,16 +26,16 @@ internal data class TvFocusedHeroModel( internal fun FocusableItem.toTvFocusedHeroModel(): TvFocusedHeroModel { return when (this) { is ContinueWatchingItem -> when (type) { - BaseItemKind.MOVIE -> movie!!.toTvFocusedHeroModel() - BaseItemKind.EPISODE -> episode!!.toTvFocusedHeroModel() + MediaKind.MOVIE -> movie!!.toTvFocusedHeroModel() + MediaKind.EPISODE -> episode!!.toTvFocusedHeroModel() else -> unsupportedType(type) } is NextUpItem -> episode.toTvFocusedHeroModel() is PosterItem -> when (type) { - BaseItemKind.MOVIE -> movie!!.toTvFocusedHeroModel() - BaseItemKind.EPISODE -> episode!!.toTvFocusedHeroModel() - BaseItemKind.SERIES -> series!!.toTvFocusedHeroModel() + MediaKind.MOVIE -> movie!!.toTvFocusedHeroModel() + MediaKind.EPISODE -> episode!!.toTvFocusedHeroModel() + MediaKind.SERIES -> series!!.toTvFocusedHeroModel() else -> unsupportedType(type) } } @@ -117,6 +117,6 @@ private fun seasonLabel(seasonCount: Int): String { return if (seasonCount == 1) "1 season" else "$seasonCount seasons" } -private fun unsupportedType(type: BaseItemKind): Nothing { +private fun unsupportedType(type: MediaKind): Nothing { throw UnsupportedOperationException("Unsupported item type: $type") } diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeContent.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeContent.kt index 156c3504..9f6c66bf 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeContent.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeContent.kt @@ -29,7 +29,7 @@ import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.PosterItem -import org.jellyfin.sdk.model.UUID +import java.util.UUID internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item" internal const val TvHomeContentViewportTag = "tv-home-content-viewport" diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeHeroState.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeHeroState.kt index 21eec08c..d0927863 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeHeroState.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeHeroState.kt @@ -10,7 +10,7 @@ import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.PosterItem -import org.jellyfin.sdk.model.UUID +import java.util.UUID internal data class TvHomeItemRegistry( val visibleLibraries: List, diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeSections.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeSections.kt index 2836b8c0..3ef0b7a8 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeSections.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/home/ui/TvHomeSections.kt @@ -52,8 +52,8 @@ import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.PosterItem -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind +import java.util.UUID +import hu.bbara.purefin.core.model.MediaKind import org.jellyfin.sdk.model.api.ImageType private val TvHomeSectionsThumbShape = RoundedCornerShape(20.dp) @@ -93,12 +93,12 @@ fun TvContinueWatchingSection( title = item.primaryText, supporting = item.secondaryText, imageUrl = when (item.type) { - BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl( + MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl( prefixImageUrl = item.movie?.imageUrlPrefix, imageType = ImageType.PRIMARY ) - BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl( + MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl( prefixImageUrl = item.episode?.imageUrlPrefix, imageType = ImageType.PRIMARY ) @@ -124,8 +124,8 @@ fun TvContinueWatchingSection( onFocusedItem = { onFocusedItem(item) }, onClick = { when (item.type) { - BaseItemKind.MOVIE -> onMovieSelected(item.movie!!.id) - BaseItemKind.EPISODE -> { + MediaKind.MOVIE -> onMovieSelected(item.movie!!.id) + MediaKind.EPISODE -> { val episode = item.episode!! onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id) } diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/library/ui/TvLibrariesOverviewScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/library/ui/TvLibrariesOverviewScreen.kt index bb25aa99..e9a692a0 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/library/ui/TvLibrariesOverviewScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/library/ui/TvLibrariesOverviewScreen.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.feature.shared.home.LibraryItem -import org.jellyfin.sdk.model.api.CollectionType +import hu.bbara.purefin.core.model.LibraryKind internal const val TvLibrariesOverviewItemTagPrefix = "tv-libraries-overview-item-" @@ -161,9 +161,8 @@ private fun TvLibraryOverviewCard( } else { Icon( imageVector = when (item.type) { - CollectionType.MOVIES -> Icons.Outlined.Movie - CollectionType.TVSHOWS -> Icons.Outlined.Tv - else -> Icons.Outlined.Collections + LibraryKind.MOVIES -> Icons.Outlined.Movie + LibraryKind.SERIES -> Icons.Outlined.Tv }, contentDescription = null, tint = scheme.onSurfaceVariant, diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/library/ui/TvLibraryScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/library/ui/TvLibraryScreen.kt index b6deea2a..fd4a41a3 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/library/ui/TvLibraryScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/library/ui/TvLibraryScreen.kt @@ -24,10 +24,10 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import hu.bbara.purefin.common.ui.PosterCard import hu.bbara.purefin.common.ui.components.PurefinIconButton -import hu.bbara.purefin.core.data.navigation.LibraryDto +import hu.bbara.purefin.feature.shared.navigation.LibraryDto import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.shared.library.LibraryViewModel -import org.jellyfin.sdk.model.UUID +import java.util.UUID @Composable fun TvLibraryScreen( diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/navigation/NavigationLocals.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/navigation/NavigationLocals.kt new file mode 100644 index 00000000..f29bcd93 --- /dev/null +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/navigation/NavigationLocals.kt @@ -0,0 +1,12 @@ +package hu.bbara.purefin.tv.navigation + +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf +import hu.bbara.purefin.feature.shared.navigation.NavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route + +val LocalNavigationManager: ProvidableCompositionLocal = + staticCompositionLocalOf { error("NavigationManager not provided") } + +val LocalNavigationBackStack = compositionLocalOf> { emptyList() } diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/navigation/TvNavigationModule.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/navigation/TvNavigationModule.kt index 29babcd9..e7336472 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/navigation/TvNavigationModule.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/navigation/TvNavigationModule.kt @@ -6,7 +6,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityRetainedComponent import dagger.multibindings.IntoSet -import hu.bbara.purefin.core.data.navigation.Route +import hu.bbara.purefin.feature.shared.navigation.Route @Module @InstallIn(ActivityRetainedComponent::class) diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/navigation/TvRouteEntryBuilder.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/navigation/TvRouteEntryBuilder.kt index 9be2c040..c32ec930 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/navigation/TvRouteEntryBuilder.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/navigation/TvRouteEntryBuilder.kt @@ -4,8 +4,7 @@ import androidx.navigation3.runtime.EntryProviderScope import hu.bbara.purefin.app.content.episode.TvEpisodeScreen import hu.bbara.purefin.app.content.movie.TvMovieScreen import hu.bbara.purefin.app.content.series.TvSeriesScreen -import hu.bbara.purefin.core.data.navigation.LocalNavigationManager -import hu.bbara.purefin.core.data.navigation.Route +import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.login.ui.LoginScreen import hu.bbara.purefin.tv.TvAppScreen import hu.bbara.purefin.tv.library.ui.TvLibraryScreen diff --git a/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt index dae60376..fe8ce43d 100644 --- a/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt +++ b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt @@ -36,16 +36,16 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory import coil3.request.crossfade import coil3.util.DebugLogger import dagger.hilt.android.AndroidEntryPoint -import hu.bbara.purefin.common.ui.PurefinWaitingScreen +import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen import hu.bbara.purefin.core.data.client.JellyfinApiClient import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor -import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack -import hu.bbara.purefin.core.data.navigation.LocalNavigationManager -import hu.bbara.purefin.core.data.navigation.NavigationCommand -import hu.bbara.purefin.core.data.navigation.NavigationManager -import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.session.UserSessionRepository -import hu.bbara.purefin.login.ui.LoginScreen +import hu.bbara.purefin.feature.shared.navigation.NavigationCommand +import hu.bbara.purefin.feature.shared.navigation.NavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route +import hu.bbara.purefin.navigation.LocalNavigationBackStack +import hu.bbara.purefin.navigation.LocalNavigationManager +import hu.bbara.purefin.ui.screen.login.LoginScreen import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.backgroundDark import kotlinx.coroutines.launch diff --git a/app/src/main/java/hu/bbara/purefin/app/home/HomeNavigation.kt b/app/src/main/java/hu/bbara/purefin/app/home/HomeNavigation.kt deleted file mode 100644 index 396067ea..00000000 --- a/app/src/main/java/hu/bbara/purefin/app/home/HomeNavigation.kt +++ /dev/null @@ -1,13 +0,0 @@ -package hu.bbara.purefin.app.home - -import androidx.navigation3.runtime.EntryProviderScope -import hu.bbara.purefin.core.data.navigation.Route - -/** - * Navigation 3 entry definition for the Home section. - */ -fun EntryProviderScope.homeSection() { - entry { - AppScreen() - } -} diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinSeachBar.kt b/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinSeachBar.kt deleted file mode 100644 index 2d39df41..00000000 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinSeachBar.kt +++ /dev/null @@ -1,111 +0,0 @@ -package hu.bbara.purefin.common.ui.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SearchBar -import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.semantics.isTraversalGroup -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.traversalIndex -import androidx.compose.ui.unit.dp -import hu.bbara.purefin.core.model.SearchResult - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PurefinSearchBar( - onQueryChange: (String) -> Unit, - onSearch: (String) -> Unit, - searchResults: List, - onExpandedChange: (Boolean) -> Unit = {}, - modifier: Modifier = Modifier -) { - var query by remember { mutableStateOf("") } - var expanded by rememberSaveable { mutableStateOf(false) } - val setExpanded: (Boolean) -> Unit = { - expanded = it - onExpandedChange(it) - } - - Box( - modifier - .fillMaxWidth() - .semantics { isTraversalGroup = true } - ) { - SearchBar( - modifier = Modifier - .align(Alignment.TopCenter) - .fillMaxWidth() - .semantics { traversalIndex = 0f }, - inputField = { - SearchBarDefaults.InputField( - query = query, - onQueryChange = { - query = it - onQueryChange(it) - }, - onSearch = { - onSearch(query) - setExpanded(false) - }, - expanded = expanded, - onExpandedChange = setExpanded, - placeholder = { Text("Search") } - ) - }, - expanded = expanded, - onExpandedChange = setExpanded, - ) { - LazyVerticalGrid( - columns = GridCells.Adaptive(minSize = 120.dp), - contentPadding = PaddingValues(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.background(MaterialTheme.colorScheme.background) - ) { - items(searchResults) { item -> - SearchResultCard(item) - } - } - } - } -} - -@Composable -private fun SearchResultCard( - item: SearchResult, - modifier: Modifier = Modifier -) { - Column(modifier = modifier) { - PurefinAsyncImage( - model = item.posterUrl, - contentDescription = item.title, - modifier = Modifier.height(150.dp), - contentScale = ContentScale.Fit - ) - Text( - text = item.title, - modifier = Modifier.padding(horizontal = 8.dp) - ) - } -} diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/SearchField.kt b/app/src/main/java/hu/bbara/purefin/common/ui/components/SearchField.kt deleted file mode 100644 index 21856d95..00000000 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/SearchField.kt +++ /dev/null @@ -1,56 +0,0 @@ -package hu.bbara.purefin.common.ui.components - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Search -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp - -@Composable -fun SearchField( - value: String, - onValueChange: (String) -> Unit, - placeholder: String, - backgroundColor: Color = Color.Unspecified, - textColor: Color = Color.Unspecified, - cursorColor: Color = Color.Unspecified, - modifier: Modifier = Modifier, -) { - val scheme = MaterialTheme.colorScheme - val resolvedBackgroundColor = - if (backgroundColor == Color.Unspecified) scheme.surfaceVariant else backgroundColor - val resolvedTextColor = if (textColor == Color.Unspecified) scheme.onSurface else textColor - val resolvedCursorColor = if (cursorColor == Color.Unspecified) scheme.primary else cursorColor - - TextField( - value = value, - onValueChange = onValueChange, - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)), - singleLine = true, - placeholder = { Text(placeholder) }, - leadingIcon = { Icon(imageVector = Icons.Outlined.Search, contentDescription = null) }, - colors = TextFieldDefaults.colors( - focusedContainerColor = resolvedBackgroundColor, - unfocusedContainerColor = resolvedBackgroundColor, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - cursorColor = resolvedCursorColor, - focusedTextColor = resolvedTextColor, - unfocusedTextColor = resolvedTextColor, - focusedLeadingIconColor = scheme.onSurfaceVariant, - unfocusedLeadingIconColor = scheme.onSurfaceVariant, - focusedPlaceholderColor = scheme.onSurfaceVariant, - unfocusedPlaceholderColor = scheme.onSurfaceVariant, - )) -} diff --git a/app/src/main/java/hu/bbara/purefin/navigation/NavigationModule.kt b/app/src/main/java/hu/bbara/purefin/modules/NavigationModule.kt similarity index 75% rename from app/src/main/java/hu/bbara/purefin/navigation/NavigationModule.kt rename to app/src/main/java/hu/bbara/purefin/modules/NavigationModule.kt index bf42c4ad..6176a23d 100644 --- a/app/src/main/java/hu/bbara/purefin/navigation/NavigationModule.kt +++ b/app/src/main/java/hu/bbara/purefin/modules/NavigationModule.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.navigation +package hu.bbara.purefin.modules import androidx.navigation3.runtime.EntryProviderScope import dagger.Module @@ -6,7 +6,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityRetainedComponent import dagger.multibindings.IntoSet -import hu.bbara.purefin.core.data.navigation.Route +import hu.bbara.purefin.feature.shared.navigation.Route +import hu.bbara.purefin.navigation.appRouteEntryBuilder @Module @InstallIn(ActivityRetainedComponent::class) diff --git a/app/src/main/java/hu/bbara/purefin/di/PlaybackProfileFamilyModule.kt b/app/src/main/java/hu/bbara/purefin/modules/PlaybackProfileFamilyModule.kt similarity index 91% rename from app/src/main/java/hu/bbara/purefin/di/PlaybackProfileFamilyModule.kt rename to app/src/main/java/hu/bbara/purefin/modules/PlaybackProfileFamilyModule.kt index 7e2cca97..33372c00 100644 --- a/app/src/main/java/hu/bbara/purefin/di/PlaybackProfileFamilyModule.kt +++ b/app/src/main/java/hu/bbara/purefin/modules/PlaybackProfileFamilyModule.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.di +package hu.bbara.purefin.modules import dagger.Module import dagger.Provides @@ -12,4 +12,4 @@ object PlaybackProfileFamilyModule { @Provides fun providePlaybackProfileFamily(): PlaybackProfileFamily = PlaybackProfileFamily.MOBILE -} +} \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/navigation/NavigationLocals.kt b/app/src/main/java/hu/bbara/purefin/navigation/NavigationLocals.kt new file mode 100644 index 00000000..04a6d775 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/navigation/NavigationLocals.kt @@ -0,0 +1,12 @@ +package hu.bbara.purefin.navigation + +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf +import hu.bbara.purefin.feature.shared.navigation.NavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route + +val LocalNavigationManager: ProvidableCompositionLocal = + staticCompositionLocalOf { error("NavigationManager not provided") } + +val LocalNavigationBackStack = compositionLocalOf> { emptyList() } diff --git a/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt b/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt index 943c52ad..bd29e4c6 100644 --- a/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt +++ b/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt @@ -1,13 +1,13 @@ package hu.bbara.purefin.navigation import androidx.navigation3.runtime.EntryProviderScope +import hu.bbara.purefin.feature.shared.navigation.Route +import hu.bbara.purefin.ui.screen.library.LibraryScreen +import hu.bbara.purefin.ui.screen.login.LoginScreen +import hu.bbara.purefin.ui.screen.AppScreen import hu.bbara.purefin.ui.screen.episode.EpisodeScreen import hu.bbara.purefin.ui.screen.movie.MovieScreen import hu.bbara.purefin.ui.screen.series.SeriesScreen -import hu.bbara.purefin.app.home.AppScreen -import hu.bbara.purefin.app.library.ui.LibraryScreen -import hu.bbara.purefin.core.data.navigation.Route -import hu.bbara.purefin.login.ui.LoginScreen fun EntryProviderScope.appRouteEntryBuilder() { entry { diff --git a/app/src/main/java/hu/bbara/purefin/player/PlayerActivity.kt b/app/src/main/java/hu/bbara/purefin/player/PlayerActivity.kt index 8b55a308..a1644600 100644 --- a/app/src/main/java/hu/bbara/purefin/player/PlayerActivity.kt +++ b/app/src/main/java/hu/bbara/purefin/player/PlayerActivity.kt @@ -12,7 +12,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import dagger.hilt.android.AndroidEntryPoint import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel -import hu.bbara.purefin.player.ui.PlayerScreen +import hu.bbara.purefin.ui.screen.player.PlayerScreen import hu.bbara.purefin.ui.theme.AppTheme @AndroidEntryPoint diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/DefaultTopBar.kt b/app/src/main/java/hu/bbara/purefin/ui/common/DefaultTopBar.kt similarity index 83% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/DefaultTopBar.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/DefaultTopBar.kt index 80368723..b5a5eb46 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/DefaultTopBar.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/DefaultTopBar.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui +package hu.bbara.purefin.ui.common import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -27,7 +27,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel -import hu.bbara.purefin.common.ui.components.PurefinSearchBar import hu.bbara.purefin.feature.shared.search.SearchViewModel @Composable @@ -56,24 +55,6 @@ fun DefaultTopBar( .fillMaxWidth(), contentAlignment = Alignment.CenterEnd ) { - PurefinSearchBar( - onQueryChange = { - searchViewModel.search(it) - }, - onSearch = { - searchViewModel.search(it) - }, - onExpandedChange = { expanded -> - isSearchExpanded = expanded - if (expanded) { - isProfileMenuExpanded = false - } - }, - searchResults = searchResult.value, - modifier = Modifier - .fillMaxWidth() - .padding(end = if (isSearchExpanded) 0.dp else 72.dp), - ) if (!isSearchExpanded) { Box { IconButton( diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/ContentBadge.kt b/app/src/main/java/hu/bbara/purefin/ui/common/badge/ContentBadge.kt similarity index 95% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/shared/ContentBadge.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/badge/ContentBadge.kt index ee7dec48..05c6d3f4 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/ContentBadge.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/badge/ContentBadge.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.shared +package hu.bbara.purefin.ui.common.badge import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/UnwatchedEpisodeIndicator.kt b/app/src/main/java/hu/bbara/purefin/ui/common/badge/UnwatchedEpisodeBadge.kt similarity index 96% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/UnwatchedEpisodeIndicator.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/badge/UnwatchedEpisodeBadge.kt index d66d617d..015ab1cc 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/UnwatchedEpisodeIndicator.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/badge/UnwatchedEpisodeBadge.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.badge import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/WatchStateIndicator.kt b/app/src/main/java/hu/bbara/purefin/ui/common/badge/WatchStateBadge.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/WatchStateIndicator.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/badge/WatchStateBadge.kt index 7e10c9f0..539b1321 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/WatchStateIndicator.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/badge/WatchStateBadge.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.badge import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaProgressBar.kt b/app/src/main/java/hu/bbara/purefin/ui/common/bar/MediaProgressBar.kt similarity index 97% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/MediaProgressBar.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/bar/MediaProgressBar.kt index 7e8d9f01..30a5d962 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaProgressBar.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/bar/MediaProgressBar.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.bar import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/GhostIconButton.kt b/app/src/main/java/hu/bbara/purefin/ui/common/button/GhostIconButton.kt similarity index 96% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/GhostIconButton.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/button/GhostIconButton.kt index 64ad605f..2c0b423f 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/GhostIconButton.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/button/GhostIconButton.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.button import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaActionButton.kt b/app/src/main/java/hu/bbara/purefin/ui/common/button/MediaActionButton.kt similarity index 95% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/MediaActionButton.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/button/MediaActionButton.kt index 8bcf97d0..8561f5ee 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaActionButton.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/button/MediaActionButton.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.button import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaResumeButton.kt b/app/src/main/java/hu/bbara/purefin/ui/common/button/MediaResumeButton.kt similarity index 95% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/MediaResumeButton.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/button/MediaResumeButton.kt index c7ce6bad..42251fe7 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaResumeButton.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/button/MediaResumeButton.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.button import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -77,7 +77,7 @@ fun MediaResumeButton( } @Composable -private fun ButtonContent(text: String, color: androidx.compose.ui.graphics.Color) { +private fun ButtonContent(text: String, color: Color) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinIconButton.kt b/app/src/main/java/hu/bbara/purefin/ui/common/button/PurefinIconButton.kt similarity index 96% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinIconButton.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/button/PurefinIconButton.kt index 9a5dc9c1..0be04aa4 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinIconButton.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/button/PurefinIconButton.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.button import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/PurefinTextButton.kt b/app/src/main/java/hu/bbara/purefin/ui/common/button/PurefinTextButton.kt similarity index 94% rename from app/src/main/java/hu/bbara/purefin/common/ui/PurefinTextButton.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/button/PurefinTextButton.kt index fa0725cb..b24fed49 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/PurefinTextButton.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/button/PurefinTextButton.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui +package hu.bbara.purefin.ui.common.button import androidx.compose.foundation.layout.RowScope import androidx.compose.material3.Button diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt b/app/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt similarity index 86% rename from app/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt index 851172d5..6620b6c4 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/PosterCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/card/PosterCard.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui +package hu.bbara.purefin.ui.common.card import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -23,12 +23,12 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.request.ImageRequest -import hu.bbara.purefin.common.ui.components.PurefinAsyncImage -import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator -import hu.bbara.purefin.common.ui.components.WatchStateIndicator import hu.bbara.purefin.feature.shared.home.PosterItem -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind +import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator +import hu.bbara.purefin.ui.common.badge.WatchStateIndicator +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage +import java.util.UUID +import hu.bbara.purefin.core.model.MediaKind @Composable fun PosterCard( @@ -47,9 +47,9 @@ fun PosterCard( fun openItem(posterItem: PosterItem) { when (posterItem.type) { - BaseItemKind.MOVIE -> onMovieSelected(posterItem.id) - BaseItemKind.SERIES -> onSeriesSelected(posterItem.id) - BaseItemKind.EPISODE -> { + MediaKind.MOVIE -> onMovieSelected(posterItem.id) + MediaKind.SERIES -> onSeriesSelected(posterItem.id) + MediaKind.EPISODE -> { val ep = posterItem.episode!! onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id) } @@ -78,7 +78,7 @@ fun PosterCard( contentScale = ContentScale.Crop ) when (item.type) { - BaseItemKind.MOVIE -> { + MediaKind.MOVIE -> { val m = item.movie!! WatchStateIndicator( size = 28, @@ -88,7 +88,7 @@ fun PosterCard( started = (m.progress ?: 0.0) > 0 ) } - BaseItemKind.EPISODE -> { + MediaKind.EPISODE -> { val ep = item.episode!! WatchStateIndicator( size = 28, @@ -98,7 +98,7 @@ fun PosterCard( started = (ep.progress ?: 0.0) > 0 ) } - BaseItemKind.SERIES -> UnwatchedEpisodeIndicator( + MediaKind.SERIES -> UnwatchedEpisodeIndicator( size = 28, modifier = Modifier.align(Alignment.TopEnd) .padding(8.dp), diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/SectionHeader.kt b/app/src/main/java/hu/bbara/purefin/ui/common/header/SectionHeader.kt similarity index 97% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/shared/SectionHeader.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/header/SectionHeader.kt index 80919eef..6408e27e 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/SectionHeader.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/header/SectionHeader.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.shared +package hu.bbara.purefin.ui.common.header import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinAsyncImage.kt b/app/src/main/java/hu/bbara/purefin/ui/common/image/PurefinAsyncImage.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinAsyncImage.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/image/PurefinAsyncImage.kt index 7f72044a..9c5ae3a6 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinAsyncImage.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/image/PurefinAsyncImage.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinLogo.kt b/app/src/main/java/hu/bbara/purefin/ui/common/image/PurefinLogo.kt similarity index 95% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinLogo.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/image/PurefinLogo.kt index 859c7d26..7aadca94 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/PurefinLogo.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/image/PurefinLogo.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.image import android.graphics.BitmapFactory import androidx.compose.foundation.Image diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/MediaDetailComponents.kt b/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaDetailComponents.kt similarity index 97% rename from app/src/main/java/hu/bbara/purefin/common/ui/MediaDetailComponents.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/media/MediaDetailComponents.kt index e3edf62b..66e6be70 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/MediaDetailComponents.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaDetailComponents.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui +package hu.bbara.purefin.ui.common.media import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -33,8 +33,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.core.model.CastMember +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage @Composable fun MediaMetaChip( diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaHero.kt b/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaHero.kt similarity index 93% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/MediaHero.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/media/MediaHero.kt index 8134404a..b566bab7 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaHero.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaHero.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.media import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -11,6 +11,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage @Composable fun MediaHero( diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaPlaybackSettings.kt b/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaPlaybackSettings.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/MediaPlaybackSettings.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/media/MediaPlaybackSettings.kt index 0d5053a9..2812fa81 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaPlaybackSettings.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaPlaybackSettings.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.media import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/MediaSynopsis.kt b/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaSynopsis.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/common/ui/MediaSynopsis.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/media/MediaSynopsis.kt index 729931d9..da86d97f 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/MediaSynopsis.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaSynopsis.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui +package hu.bbara.purefin.ui.common.media import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/PurefinComplexTextField.kt b/app/src/main/java/hu/bbara/purefin/ui/common/textfield/PurefinComplexTextField.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/common/ui/PurefinComplexTextField.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/textfield/PurefinComplexTextField.kt index 33c97d0e..ae1e2188 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/PurefinComplexTextField.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/textfield/PurefinComplexTextField.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui +package hu.bbara.purefin.ui.common.textfield import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/PurefinPasswordField.kt b/app/src/main/java/hu/bbara/purefin/ui/common/textfield/PurefinPasswordField.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/common/ui/PurefinPasswordField.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/textfield/PurefinPasswordField.kt index 2b3a1cf7..96b8150d 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/PurefinPasswordField.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/textfield/PurefinPasswordField.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui +package hu.bbara.purefin.ui.common.textfield import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/TimedVisibility.kt b/app/src/main/java/hu/bbara/purefin/ui/common/visual/TimedVisibility.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/common/ui/components/TimedVisibility.kt rename to app/src/main/java/hu/bbara/purefin/ui/common/visual/TimedVisibility.kt index 89fbb71e..2a48151f 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/TimedVisibility.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/visual/TimedVisibility.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui.components +package hu.bbara.purefin.ui.common.visual import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition diff --git a/app/src/main/java/hu/bbara/purefin/app/home/AppBottomBar.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/AppBottomBar.kt similarity index 97% rename from app/src/main/java/hu/bbara/purefin/app/home/AppBottomBar.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/AppBottomBar.kt index 095bfcb0..f93b74f3 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/AppBottomBar.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/AppBottomBar.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home +package hu.bbara.purefin.ui.screen import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Collections diff --git a/app/src/main/java/hu/bbara/purefin/app/home/AppScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt similarity index 85% rename from app/src/main/java/hu/bbara/purefin/app/home/AppScreen.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt index ed676d45..da4a2b9b 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/AppScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home +package hu.bbara.purefin.ui.screen import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -10,8 +10,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LifecycleResumeEffect -import hu.bbara.purefin.app.home.ui.HomeNavItem import hu.bbara.purefin.feature.shared.home.AppViewModel +import hu.bbara.purefin.ui.screen.download.DownloadsScreen +import hu.bbara.purefin.ui.screen.home.HomeScreen +import hu.bbara.purefin.ui.screen.home.components.HomeNavItem +import hu.bbara.purefin.ui.screen.libraries.LibrariesScreen @Composable fun AppScreen( @@ -52,7 +55,12 @@ fun AppScreen( onMovieSelected = viewModel::onMovieSelected, onSeriesSelected = viewModel::onSeriesSelected, onEpisodeSelected = viewModel::onEpisodeSelected, - onLibrarySelected = { library -> viewModel.onLibrarySelected(library.id, library.name) }, + onLibrarySelected = { library -> + viewModel.onLibrarySelected( + library.id, + library.name + ) + }, onProfileClick = {}, onSettingsClick = {}, onLogoutClick = viewModel::logout, diff --git a/app/src/main/java/hu/bbara/purefin/app/home/DownloadsScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/download/DownloadsScreen.kt similarity index 84% rename from app/src/main/java/hu/bbara/purefin/app/home/DownloadsScreen.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/download/DownloadsScreen.kt index 65b82987..942195d5 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/DownloadsScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/download/DownloadsScreen.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home +package hu.bbara.purefin.ui.screen.download import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -6,7 +6,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import hu.bbara.purefin.app.home.ui.downloads.DownloadsContent +import hu.bbara.purefin.ui.screen.download.components.DownloadsContent +import hu.bbara.purefin.ui.screen.AppBottomBar @Composable fun DownloadsScreen( diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadingItemRow.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/download/components/DownloadingItemRow.kt similarity index 97% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadingItemRow.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/download/components/DownloadingItemRow.kt index 99237cb3..e6a62591 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadingItemRow.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/download/components/DownloadingItemRow.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.downloads +package hu.bbara.purefin.ui.screen.download.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -26,7 +26,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import hu.bbara.purefin.common.ui.components.PurefinAsyncImage +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.feature.shared.download.ActiveDownloadItem @Composable diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadsContent.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/download/components/DownloadsContent.kt similarity index 97% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadsContent.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/download/components/DownloadsContent.kt index af4fbc94..64950402 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/downloads/DownloadsContent.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/download/components/DownloadsContent.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.downloads +package hu.bbara.purefin.ui.screen.download.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import hu.bbara.purefin.common.ui.PosterCard +import hu.bbara.purefin.ui.common.card.PosterCard import hu.bbara.purefin.feature.shared.download.DownloadsViewModel @Composable diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt index c6f5cc56..ff9ed682 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt @@ -34,14 +34,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import hu.bbara.purefin.common.ui.MediaMetaChip -import hu.bbara.purefin.common.ui.PurefinWaitingScreen -import hu.bbara.purefin.common.ui.components.MediaHero +import hu.bbara.purefin.ui.common.media.MediaMetaChip +import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen +import hu.bbara.purefin.ui.common.media.MediaHero import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.data.image.JellyfinImageHelper -import hu.bbara.purefin.core.data.navigation.EpisodeDto -import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack -import hu.bbara.purefin.core.data.navigation.Route +import hu.bbara.purefin.feature.shared.navigation.EpisodeDto +import hu.bbara.purefin.navigation.LocalNavigationBackStack +import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/episode/components/EpisodeComponents.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/episode/components/EpisodeComponents.kt index eefae6d5..295d652d 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/episode/components/EpisodeComponents.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/episode/components/EpisodeComponents.kt @@ -34,12 +34,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import hu.bbara.purefin.common.ui.MediaCastRow -import hu.bbara.purefin.common.ui.MediaSynopsis -import hu.bbara.purefin.common.ui.components.GhostIconButton -import hu.bbara.purefin.common.ui.components.MediaActionButton -import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings -import hu.bbara.purefin.common.ui.components.MediaResumeButton +import hu.bbara.purefin.ui.common.media.MediaCastRow +import hu.bbara.purefin.ui.common.media.MediaSynopsis +import hu.bbara.purefin.ui.common.button.GhostIconButton +import hu.bbara.purefin.ui.common.button.MediaActionButton +import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings +import hu.bbara.purefin.ui.common.button.MediaResumeButton import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.player.PlayerActivity diff --git a/app/src/main/java/hu/bbara/purefin/app/home/HomeScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/HomeScreen.kt similarity index 92% rename from app/src/main/java/hu/bbara/purefin/app/home/HomeScreen.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/HomeScreen.kt index 03e846fe..ad70e3e6 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/HomeScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/HomeScreen.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home +package hu.bbara.purefin.ui.screen.home import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -13,15 +13,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import hu.bbara.purefin.app.home.ui.HomeContent -import hu.bbara.purefin.app.home.ui.HomeTopBar -import hu.bbara.purefin.app.home.ui.search.HomeSearchOverlay +import hu.bbara.purefin.ui.screen.home.components.HomeContent +import hu.bbara.purefin.ui.screen.home.components.HomeTopBar +import hu.bbara.purefin.ui.screen.home.components.search.HomeSearchOverlay import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.shared.home.SuggestedItem -import org.jellyfin.sdk.model.UUID +import hu.bbara.purefin.ui.screen.AppBottomBar +import java.util.UUID @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt similarity index 89% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt index ae4c4296..6990ff46 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui +package hu.bbara.purefin.ui.screen.home.components import android.util.Log import androidx.compose.foundation.background @@ -22,18 +22,17 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import hu.bbara.purefin.app.home.ui.continuewatching.ContinueWatchingSection -import hu.bbara.purefin.app.home.ui.featured.SuggestionsSection -import hu.bbara.purefin.app.home.ui.library.LibraryPosterSection -import hu.bbara.purefin.app.home.ui.nextup.NextUpSection -import hu.bbara.purefin.app.home.ui.shared.HomeEmptyState import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.shared.home.SuggestedItem -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind +import hu.bbara.purefin.ui.screen.home.components.continuewatching.ContinueWatchingSection +import hu.bbara.purefin.ui.screen.home.components.featured.SuggestionsSection +import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection +import hu.bbara.purefin.ui.screen.home.components.nextup.NextUpSection +import java.util.UUID +import hu.bbara.purefin.core.model.MediaKind @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -106,9 +105,9 @@ fun HomeContent( items = suggestions, onItemOpen = { item -> when (item.type) { - BaseItemKind.MOVIE -> onMovieSelected(item.id) - BaseItemKind.SERIES -> onSeriesSelected(item.id) - BaseItemKind.EPISODE -> onEpisodeSelected(item.id, item.id, item.id) + MediaKind.MOVIE -> onMovieSelected(item.id) + MediaKind.SERIES -> onSeriesSelected(item.id) + MediaKind.EPISODE -> onEpisodeSelected(item.id, item.id, item.id) else -> { Log.e("HomeContent", "Unsupported item type: ${item.type}") } diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/HomeEmptyState.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeEmptyState.kt similarity index 96% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/shared/HomeEmptyState.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeEmptyState.kt index 6c8976e9..1c96ac72 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/shared/HomeEmptyState.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeEmptyState.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.shared +package hu.bbara.purefin.ui.screen.home.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import hu.bbara.purefin.ui.common.badge.ContentBadge @Composable fun HomeEmptyState( diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeModels.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeModels.kt similarity index 72% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/HomeModels.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeModels.kt index acfa1854..2f00cd8a 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeModels.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeModels.kt @@ -1,6 +1,6 @@ -package hu.bbara.purefin.app.home.ui +package hu.bbara.purefin.ui.screen.home.components -import org.jellyfin.sdk.model.UUID +import java.util.UUID data class HomeNavItem( val id: UUID, diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeTopBar.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeTopBar.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/HomeTopBar.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeTopBar.kt index d2230d1f..97a86d5e 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeTopBar.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeTopBar.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui +package hu.bbara.purefin.ui.screen.home.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -31,7 +31,7 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import hu.bbara.purefin.common.ui.components.PurefinLogo +import hu.bbara.purefin.ui.common.image.PurefinLogo @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingCard.kt similarity index 89% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingCard.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingCard.kt index 4f4b4eaf..18d34143 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingCard.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.continuewatching +package hu.bbara.purefin.ui.screen.home.components.continuewatching import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -23,12 +23,12 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import hu.bbara.purefin.common.ui.components.MediaProgressBar -import hu.bbara.purefin.common.ui.components.PurefinAsyncImage +import hu.bbara.purefin.ui.common.bar.MediaProgressBar +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind +import java.util.UUID +import hu.bbara.purefin.core.model.MediaKind import org.jellyfin.sdk.model.api.ImageType @Composable @@ -40,12 +40,12 @@ internal fun ContinueWatchingCard( ) { val scheme = MaterialTheme.colorScheme val supportingText = when (item.type) { - BaseItemKind.MOVIE -> listOf( + MediaKind.MOVIE -> listOf( item.movie?.year, item.movie?.runtime ).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ") - BaseItemKind.EPISODE -> listOf( + MediaKind.EPISODE -> listOf( "Episode ${item.episode?.index}", item.episode?.runtime ).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ") @@ -53,11 +53,11 @@ internal fun ContinueWatchingCard( else -> "" } val imageUrl = when (item.type) { - BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl( + MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl( prefixImageUrl = item.movie?.imageUrlPrefix, imageType = ImageType.PRIMARY ) - BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl( + MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl( prefixImageUrl = item.episode?.imageUrlPrefix, imageType = ImageType.PRIMARY ) @@ -74,8 +74,8 @@ internal fun ContinueWatchingCard( .fillMaxWidth() .clickable { when (item.type) { - BaseItemKind.MOVIE -> onMovieSelected(item.movie!!.id) - BaseItemKind.EPISODE -> { + MediaKind.MOVIE -> onMovieSelected(item.movie!!.id) + MediaKind.EPISODE -> { val episode = item.episode!! onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id) } diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingSection.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingSection.kt similarity index 90% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingSection.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingSection.kt index c185a55f..dcc0a216 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/continuewatching/ContinueWatchingSection.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingSection.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.continuewatching +package hu.bbara.purefin.ui.screen.home.components.continuewatching import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -9,9 +9,9 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import hu.bbara.purefin.app.home.ui.shared.SectionHeader +import hu.bbara.purefin.ui.common.header.SectionHeader import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem -import org.jellyfin.sdk.model.UUID +import java.util.UUID @Composable fun ContinueWatchingSection( diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/featured/SuggestionCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/featured/SuggestionCard.kt similarity index 96% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/featured/SuggestionCard.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/featured/SuggestionCard.kt index ea0a310a..1a504604 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/featured/SuggestionCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/featured/SuggestionCard.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.featured +package hu.bbara.purefin.ui.screen.home.components.featured import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -24,8 +24,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import hu.bbara.purefin.common.ui.components.MediaProgressBar -import hu.bbara.purefin.common.ui.components.PurefinAsyncImage +import hu.bbara.purefin.ui.common.bar.MediaProgressBar +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.feature.shared.home.SuggestedItem @Composable diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/featured/SuggestionsSection.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/featured/SuggestionsSection.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/featured/SuggestionsSection.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/featured/SuggestionsSection.kt index e244b85d..23e1db07 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/featured/SuggestionsSection.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/featured/SuggestionsSection.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.featured +package hu.bbara.purefin.ui.screen.home.components.featured import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/library/HomeBrowseCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/HomeBrowseCard.kt similarity index 86% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/library/HomeBrowseCard.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/HomeBrowseCard.kt index 5e1f4cf3..de4cb1fe 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/library/HomeBrowseCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/HomeBrowseCard.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.library +package hu.bbara.purefin.ui.screen.home.components.library import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -23,12 +23,12 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import hu.bbara.purefin.common.ui.components.PurefinAsyncImage -import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator -import hu.bbara.purefin.common.ui.components.WatchStateIndicator +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage +import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator +import hu.bbara.purefin.ui.common.badge.WatchStateIndicator import hu.bbara.purefin.feature.shared.home.PosterItem -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind +import java.util.UUID +import hu.bbara.purefin.core.model.MediaKind @Composable internal fun HomeBrowseCard( @@ -40,16 +40,16 @@ internal fun HomeBrowseCard( ) { val scheme = MaterialTheme.colorScheme val supportingText = when (item.type) { - BaseItemKind.MOVIE -> listOf( + MediaKind.MOVIE -> listOf( item.movie?.year, item.movie?.runtime ).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ") - BaseItemKind.SERIES -> item.series!!.let { series -> + MediaKind.SERIES -> item.series!!.let { series -> if (series.seasonCount == 1) "1 season" else "${series.seasonCount} seasons" } - BaseItemKind.EPISODE -> listOf( + MediaKind.EPISODE -> listOf( "Episode ${item.episode?.index}", item.episode?.runtime ).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ") @@ -67,9 +67,9 @@ internal fun HomeBrowseCard( .fillMaxWidth() .clickable { when (item.type) { - BaseItemKind.MOVIE -> onMovieSelected(item.id) - BaseItemKind.SERIES -> onSeriesSelected(item.id) - BaseItemKind.EPISODE -> { + MediaKind.MOVIE -> onMovieSelected(item.id) + MediaKind.SERIES -> onSeriesSelected(item.id) + MediaKind.EPISODE -> { val episode = item.episode!! onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id) } @@ -92,7 +92,7 @@ internal fun HomeBrowseCard( contentScale = ContentScale.Crop ) when (item.type) { - BaseItemKind.MOVIE -> { + MediaKind.MOVIE -> { val movie = item.movie!! WatchStateIndicator( size = 28, @@ -104,7 +104,7 @@ internal fun HomeBrowseCard( ) } - BaseItemKind.EPISODE -> { + MediaKind.EPISODE -> { val episode = item.episode!! WatchStateIndicator( size = 28, @@ -116,7 +116,7 @@ internal fun HomeBrowseCard( ) } - BaseItemKind.SERIES -> { + MediaKind.SERIES -> { UnwatchedEpisodeIndicator( size = 28, modifier = Modifier diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/library/LibraryPosterSection.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/LibraryPosterSection.kt similarity index 92% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/library/LibraryPosterSection.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/LibraryPosterSection.kt index cd2ff9de..c0951d40 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/library/LibraryPosterSection.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/LibraryPosterSection.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.library +package hu.bbara.purefin.ui.screen.home.components.library import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -9,10 +9,10 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import hu.bbara.purefin.app.home.ui.shared.SectionHeader import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.shared.home.PosterItem -import org.jellyfin.sdk.model.UUID +import hu.bbara.purefin.ui.common.header.SectionHeader +import java.util.UUID @Composable fun LibraryPosterSection( diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpCard.kt similarity index 96% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpCard.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpCard.kt index 91e8a99f..eb8a6e92 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpCard.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.nextup +package hu.bbara.purefin.ui.screen.home.components.nextup import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -22,10 +22,10 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import hu.bbara.purefin.common.ui.components.PurefinAsyncImage +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.feature.shared.home.NextUpItem -import org.jellyfin.sdk.model.UUID +import java.util.UUID import org.jellyfin.sdk.model.api.ImageType @Composable diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpSection.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpSection.kt similarity index 90% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpSection.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpSection.kt index fdbe3056..400d00aa 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/nextup/NextUpSection.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpSection.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.nextup +package hu.bbara.purefin.ui.screen.home.components.nextup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -9,9 +9,9 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import hu.bbara.purefin.app.home.ui.shared.SectionHeader +import hu.bbara.purefin.ui.common.header.SectionHeader import hu.bbara.purefin.feature.shared.home.NextUpItem -import org.jellyfin.sdk.model.UUID +import java.util.UUID @Composable fun NextUpSection( diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchOverlay.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/HomeSearchOverlay.kt similarity index 95% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchOverlay.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/HomeSearchOverlay.kt index 79e67bb1..b8253fb7 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchOverlay.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/HomeSearchOverlay.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.search +package hu.bbara.purefin.ui.screen.home.components.search import androidx.activity.compose.BackHandler import androidx.compose.foundation.background @@ -36,8 +36,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import hu.bbara.purefin.feature.shared.search.SearchViewModel -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind +import java.util.UUID +import hu.bbara.purefin.core.model.MediaKind @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -142,8 +142,8 @@ fun HomeSearchOverlay( item = item, onClick = { when (item.type) { - BaseItemKind.MOVIE -> onMovieSelected(item.id) - BaseItemKind.SERIES -> onSeriesSelected(item.id) + MediaKind.MOVIE -> onMovieSelected(item.id) + MediaKind.SERIES -> onSeriesSelected(item.id) else -> Unit } } diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchResultCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/HomeSearchResultCard.kt similarity index 87% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchResultCard.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/HomeSearchResultCard.kt index 9d083652..46a9e51e 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/search/HomeSearchResultCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/HomeSearchResultCard.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.search +package hu.bbara.purefin.ui.screen.home.components.search import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -17,9 +17,9 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import hu.bbara.purefin.common.ui.components.PurefinAsyncImage -import hu.bbara.purefin.core.model.SearchResult -import org.jellyfin.sdk.model.api.BaseItemKind +import hu.bbara.purefin.core.model.MediaKind +import hu.bbara.purefin.feature.shared.search.SearchResult +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage @Composable internal fun HomeSearchResultCard( @@ -59,8 +59,8 @@ internal fun HomeSearchResultCard( ) Text( text = when (item.type) { - BaseItemKind.MOVIE -> "Movie" - BaseItemKind.SERIES -> "Series" + MediaKind.MOVIE -> "Movie" + MediaKind.SERIES -> "Series" else -> "Title" }, style = MaterialTheme.typography.labelMedium, diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/search/SearchMessage.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/SearchMessage.kt similarity index 96% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/search/SearchMessage.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/SearchMessage.kt index 5608da91..85e3fe6a 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/search/SearchMessage.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/SearchMessage.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.search +package hu.bbara.purefin.ui.screen.home.components.search import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/app/src/main/java/hu/bbara/purefin/app/home/LibrariesScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/libraries/LibrariesScreen.kt similarity index 82% rename from app/src/main/java/hu/bbara/purefin/app/home/LibrariesScreen.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/libraries/LibrariesScreen.kt index 3872d745..246e3fc3 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/LibrariesScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/libraries/LibrariesScreen.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home +package hu.bbara.purefin.ui.screen.libraries import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -6,9 +6,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import hu.bbara.purefin.app.home.ui.DefaultTopBar -import hu.bbara.purefin.app.home.ui.HomeNavItem -import hu.bbara.purefin.app.home.ui.libraries.LibrariesContent +import hu.bbara.purefin.ui.common.DefaultTopBar +import hu.bbara.purefin.ui.screen.home.components.HomeNavItem +import hu.bbara.purefin.ui.screen.AppBottomBar +import hu.bbara.purefin.ui.screen.libraries.components.LibrariesContent @Composable fun LibrariesScreen( diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibrariesContent.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/libraries/components/LibrariesContent.kt similarity index 90% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibrariesContent.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/libraries/components/LibrariesContent.kt index 9655fc6c..699ec209 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibrariesContent.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/libraries/components/LibrariesContent.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.libraries +package hu.bbara.purefin.ui.screen.libraries.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -9,7 +9,7 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import hu.bbara.purefin.app.home.ui.HomeNavItem +import hu.bbara.purefin.ui.screen.home.components.HomeNavItem @Composable fun LibrariesContent( diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibraryListItem.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/libraries/components/LibraryListItem.kt similarity index 85% rename from app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibraryListItem.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/libraries/components/LibraryListItem.kt index 1fb99348..468a86cc 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/libraries/LibraryListItem.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/libraries/components/LibraryListItem.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.home.ui.libraries +package hu.bbara.purefin.ui.screen.libraries.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -11,8 +11,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp -import hu.bbara.purefin.app.home.ui.HomeNavItem -import hu.bbara.purefin.common.ui.components.PurefinAsyncImage +import hu.bbara.purefin.ui.screen.home.components.HomeNavItem +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage @Composable fun LibraryListItem( diff --git a/app/src/main/java/hu/bbara/purefin/app/library/ui/LibraryScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt similarity index 93% rename from app/src/main/java/hu/bbara/purefin/app/library/ui/LibraryScreen.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt index a3406d3c..7c8940bd 100644 --- a/app/src/main/java/hu/bbara/purefin/app/library/ui/LibraryScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.app.library.ui +package hu.bbara.purefin.ui.screen.library import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -21,9 +21,9 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import hu.bbara.purefin.common.ui.PosterCard -import hu.bbara.purefin.common.ui.components.PurefinIconButton -import hu.bbara.purefin.core.data.navigation.LibraryDto +import hu.bbara.purefin.ui.common.card.PosterCard +import hu.bbara.purefin.ui.common.button.PurefinIconButton +import hu.bbara.purefin.feature.shared.navigation.LibraryDto import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.shared.library.LibraryViewModel diff --git a/app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/login/LoginScreen.kt similarity index 96% rename from app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/login/LoginScreen.kt index 2291d44d..c72a81be 100644 --- a/app/src/main/java/hu/bbara/purefin/login/ui/LoginScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/login/LoginScreen.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.login.ui +package hu.bbara.purefin.ui.screen.login import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -37,10 +37,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import hu.bbara.purefin.common.ui.PurefinComplexTextField -import hu.bbara.purefin.common.ui.PurefinPasswordField -import hu.bbara.purefin.common.ui.PurefinTextButton -import hu.bbara.purefin.common.ui.PurefinWaitingScreen +import hu.bbara.purefin.ui.common.textfield.PurefinComplexTextField +import hu.bbara.purefin.ui.common.textfield.PurefinPasswordField +import hu.bbara.purefin.ui.common.button.PurefinTextButton +import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen import hu.bbara.purefin.feature.shared.login.LoginViewModel import kotlinx.coroutines.launch diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt index d42fcca5..bceb6e10 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt @@ -32,12 +32,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import hu.bbara.purefin.common.ui.MediaMetaChip -import hu.bbara.purefin.common.ui.PurefinWaitingScreen -import hu.bbara.purefin.common.ui.components.MediaHero +import hu.bbara.purefin.ui.common.media.MediaMetaChip +import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen +import hu.bbara.purefin.ui.common.media.MediaHero import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.data.image.JellyfinImageHelper -import hu.bbara.purefin.core.data.navigation.MovieDto +import hu.bbara.purefin.feature.shared.navigation.MovieDto import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/movie/components/MovieComponents.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/movie/components/MovieComponents.kt index a73dbdb9..c455aa59 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/movie/components/MovieComponents.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/movie/components/MovieComponents.kt @@ -29,12 +29,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import hu.bbara.purefin.common.ui.MediaCastRow -import hu.bbara.purefin.common.ui.MediaSynopsis -import hu.bbara.purefin.common.ui.components.GhostIconButton -import hu.bbara.purefin.common.ui.components.MediaActionButton -import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings -import hu.bbara.purefin.common.ui.components.MediaResumeButton +import hu.bbara.purefin.ui.common.media.MediaCastRow +import hu.bbara.purefin.ui.common.media.MediaSynopsis +import hu.bbara.purefin.ui.common.button.GhostIconButton +import hu.bbara.purefin.ui.common.button.MediaActionButton +import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings +import hu.bbara.purefin.ui.common.button.MediaResumeButton import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.player.PlayerActivity diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/PlayerScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/PlayerScreen.kt similarity index 92% rename from app/src/main/java/hu/bbara/purefin/player/ui/PlayerScreen.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/player/PlayerScreen.kt index e286f380..a310fd94 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/PlayerScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/PlayerScreen.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.player.ui +package hu.bbara.purefin.ui.screen.player import android.app.Activity import android.content.Context @@ -37,16 +37,16 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.util.UnstableApi import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView -import hu.bbara.purefin.common.ui.components.EmptyValueTimedVisibility -import hu.bbara.purefin.common.ui.components.ValueChangeTimedVisibility import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel -import hu.bbara.purefin.player.ui.components.PersistentOverlayContainer -import hu.bbara.purefin.player.ui.components.PlayerAdjustmentIndicator -import hu.bbara.purefin.player.ui.components.PlayerControlsOverlay -import hu.bbara.purefin.player.ui.components.PlayerGesturesLayer -import hu.bbara.purefin.player.ui.components.PlayerLoadingErrorEndCard -import hu.bbara.purefin.player.ui.components.PlayerQueuePanel -import hu.bbara.purefin.player.ui.components.rememberPersistentOverlayController +import hu.bbara.purefin.ui.common.visual.EmptyValueTimedVisibility +import hu.bbara.purefin.ui.common.visual.ValueChangeTimedVisibility +import hu.bbara.purefin.ui.screen.player.components.PersistentOverlayContainer +import hu.bbara.purefin.ui.screen.player.components.PlayerAdjustmentIndicator +import hu.bbara.purefin.ui.screen.player.components.PlayerControlsOverlay +import hu.bbara.purefin.ui.screen.player.components.PlayerGesturesLayer +import hu.bbara.purefin.ui.screen.player.components.PlayerLoadingErrorEndCard +import hu.bbara.purefin.ui.screen.player.components.PlayerQueuePanel +import hu.bbara.purefin.ui.screen.player.components.rememberPersistentOverlayController import kotlin.math.abs import kotlin.math.roundToInt @@ -101,7 +101,7 @@ fun PlayerScreen( onTap = { viewModel.toggleControlsVisibility() }, onDoubleTapRight = { viewModel.seekBy(30_000) }, onDoubleTapLeft = { viewModel.seekBy(-10_000) }, - onDoubleTapCenter = {viewModel.togglePlayPause()}, + onDoubleTapCenter = { viewModel.togglePlayPause() }, onVerticalDragLeft = { delta -> val diff = (-delta / 800f) brightness = (brightness + diff).coerceIn(0f, 1f) diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/PersistentOverlayContainer.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PersistentOverlayContainer.kt similarity index 97% rename from app/src/main/java/hu/bbara/purefin/player/ui/components/PersistentOverlayContainer.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PersistentOverlayContainer.kt index d00c31fb..5bbb7b78 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/components/PersistentOverlayContainer.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PersistentOverlayContainer.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.player.ui.components +package hu.bbara.purefin.ui.screen.player.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerAdjustmentIndicator.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerAdjustmentIndicator.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerAdjustmentIndicator.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerAdjustmentIndicator.kt index 77fe8d39..f6dd4930 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerAdjustmentIndicator.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerAdjustmentIndicator.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.player.ui.components +package hu.bbara.purefin.ui.screen.player.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerControlsOverlay.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerControlsOverlay.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerControlsOverlay.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerControlsOverlay.kt index c2dcafdd..79e55152 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerControlsOverlay.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerControlsOverlay.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.player.ui.components +package hu.bbara.purefin.ui.screen.player.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -35,8 +35,8 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import hu.bbara.purefin.common.ui.components.GhostIconButton -import hu.bbara.purefin.common.ui.components.PurefinIconButton +import hu.bbara.purefin.ui.common.button.GhostIconButton +import hu.bbara.purefin.ui.common.button.PurefinIconButton import hu.bbara.purefin.core.player.model.PlayerUiState import hu.bbara.purefin.core.player.model.TrackOption diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerGesturesLayer.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerGesturesLayer.kt similarity index 99% rename from app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerGesturesLayer.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerGesturesLayer.kt index 2e47fb70..557fa47b 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerGesturesLayer.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerGesturesLayer.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.player.ui.components +package hu.bbara.purefin.ui.screen.player.components import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerLoadingErrorEndCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerLoadingErrorEndCard.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerLoadingErrorEndCard.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerLoadingErrorEndCard.kt index 5382810a..992da239 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerLoadingErrorEndCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerLoadingErrorEndCard.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.player.ui.components +package hu.bbara.purefin.ui.screen.player.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerQueuePanel.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerQueuePanel.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerQueuePanel.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerQueuePanel.kt index c271a655..86b05a23 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerQueuePanel.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerQueuePanel.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.player.ui.components +package hu.bbara.purefin.ui.screen.player.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -30,7 +30,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import hu.bbara.purefin.common.ui.components.PurefinAsyncImage +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.core.player.model.PlayerUiState @Composable diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerSeekBar.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerSeekBar.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerSeekBar.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerSeekBar.kt index 7c479d5b..33b7bf74 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerSeekBar.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerSeekBar.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.player.ui.components +package hu.bbara.purefin.ui.screen.player.components import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/TrackSelectionButtons.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/TrackSelectionButtons.kt similarity index 98% rename from app/src/main/java/hu/bbara/purefin/player/ui/components/TrackSelectionButtons.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/player/components/TrackSelectionButtons.kt index 227cd017..0da40b24 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/components/TrackSelectionButtons.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/TrackSelectionButtons.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.player.ui.components +package hu.bbara.purefin.ui.screen.player.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -35,7 +35,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import hu.bbara.purefin.common.ui.components.PurefinIconButton +import hu.bbara.purefin.ui.common.button.PurefinIconButton import hu.bbara.purefin.core.player.model.TrackOption @Composable diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt index dae35004..936ad07f 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt @@ -30,12 +30,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import hu.bbara.purefin.common.ui.MediaSynopsis -import hu.bbara.purefin.common.ui.PurefinWaitingScreen -import hu.bbara.purefin.common.ui.components.MediaHero +import hu.bbara.purefin.ui.common.media.MediaSynopsis +import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen +import hu.bbara.purefin.ui.common.media.MediaHero import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.data.image.JellyfinImageHelper -import hu.bbara.purefin.core.data.navigation.SeriesDto +import hu.bbara.purefin.feature.shared.navigation.SeriesDto import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Season diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/series/components/SeriesComponents.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/series/components/SeriesComponents.kt index a25c3e5f..c1418033 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/series/components/SeriesComponents.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/series/components/SeriesComponents.kt @@ -57,19 +57,19 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import hu.bbara.purefin.common.ui.MediaCastRow -import hu.bbara.purefin.common.ui.MediaMetaChip -import hu.bbara.purefin.common.ui.components.GhostIconButton -import hu.bbara.purefin.common.ui.components.MediaActionButton -import hu.bbara.purefin.common.ui.components.MediaProgressBar -import hu.bbara.purefin.common.ui.components.MediaResumeButton -import hu.bbara.purefin.common.ui.components.PurefinAsyncImage -import hu.bbara.purefin.common.ui.components.WatchStateIndicator +import hu.bbara.purefin.ui.common.media.MediaCastRow +import hu.bbara.purefin.ui.common.media.MediaMetaChip +import hu.bbara.purefin.ui.common.button.GhostIconButton +import hu.bbara.purefin.ui.common.button.MediaActionButton +import hu.bbara.purefin.ui.common.bar.MediaProgressBar +import hu.bbara.purefin.ui.common.button.MediaResumeButton +import hu.bbara.purefin.ui.common.image.PurefinAsyncImage +import hu.bbara.purefin.ui.common.badge.WatchStateIndicator import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.data.image.JellyfinImageHelper -import hu.bbara.purefin.core.data.navigation.EpisodeDto -import hu.bbara.purefin.core.data.navigation.LocalNavigationManager -import hu.bbara.purefin.core.data.navigation.Route +import hu.bbara.purefin.feature.shared.navigation.EpisodeDto +import hu.bbara.purefin.navigation.LocalNavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Season diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/WaitingScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/waiting/WaitingScreen.kt similarity index 99% rename from app/src/main/java/hu/bbara/purefin/common/ui/WaitingScreen.kt rename to app/src/main/java/hu/bbara/purefin/ui/screen/waiting/WaitingScreen.kt index 6ba67bf8..7545732e 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/WaitingScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/waiting/WaitingScreen.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.common.ui +package hu.bbara.purefin.ui.screen.waiting import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.RepeatMode diff --git a/build.gradle.kts b/build.gradle.kts index 5950d80e..da309545 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,8 +3,9 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.hilt) apply false -} \ No newline at end of file +} diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index b4cc865b..92aec6a9 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -3,7 +3,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.hilt) alias(libs.plugins.ksp) @@ -21,10 +20,6 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - - buildFeatures { - compose = true - } } kotlin { @@ -43,12 +38,7 @@ dependencies { implementation(libs.datastore) implementation(libs.okhttp) implementation(libs.logging.interceptor) - implementation(libs.coil.compose) - implementation(libs.coil.network.okhttp) implementation(libs.androidx.room.ktx) ksp(libs.androidx.room.compiler) - implementation(libs.androidx.navigation3.runtime) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.ui) testImplementation(libs.junit) } diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/CompositeMediaRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/CompositeMediaRepository.kt index 0b834057..fba74caa 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/CompositeMediaRepository.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/CompositeMediaRepository.kt @@ -1,10 +1,11 @@ package hu.bbara.purefin.core.data -import android.util.Log -import hu.bbara.purefin.core.data.session.UserSessionRepository import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Series +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -12,41 +13,22 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn -import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton -/** - * Switches between [OfflineMediaRepository] and [AppContentRepository] based on - * [UserSessionRepository.isOfflineMode]. When offline mode is enabled, all reads - * and writes go through the offline (downloaded) repository; otherwise the online - * repository is used. - */ @OptIn(ExperimentalCoroutinesApi::class) @Singleton class CompositeMediaRepository @Inject constructor( private val offlineRepository: OfflineMediaRepository, private val onlineRepository: InMemoryMediaRepository, private val networkMonitor: NetworkMonitor, -) : MediaRepository { +) : MediaCatalogReader, MediaProgressWriter { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - override val ready: StateFlow = combine( - offlineRepository.ready, - onlineRepository.ready - ) { offlineReady, onlineReady -> - offlineReady && onlineReady - }.stateIn(scope, SharingStarted.Eagerly, false) - - private val isOnline: StateFlow = networkMonitor.isOnline - .stateIn(scope, SharingStarted.Eagerly, false) - - private val activeRepository: Flow = + private val activeRepository: Flow = networkMonitor.isOnline.flatMapLatest { online -> flowOf(if (online) onlineRepository else offlineRepository) } @@ -63,42 +45,12 @@ class CompositeMediaRepository @Inject constructor( .flatMapLatest { it.episodes } .stateIn(scope, SharingStarted.Eagerly, emptyMap()) - override fun upsertMovies(movies: List) { - if (!isOnline.value) { - Log.e("CompositeMediaRepository", "upsertMovies called in offline mode") - return - } - onlineRepository.upsertMovies(movies) - } - - override fun upsertSeries(series: List) { - if (!isOnline.value) { - Log.e("CompositeMediaRepository", "upsertSeries called in offline mode") - return - } - onlineRepository.upsertSeries(series) - } - - override fun upsertEpisodes(episodes: List) { - if (!isOnline.value) { - Log.e("CompositeMediaRepository", "upsertEpisodes called in offline mode") - return - } - onlineRepository.upsertEpisodes(episodes) - } - override fun observeSeriesWithContent(seriesId: UUID): Flow { return activeRepository.flatMapLatest { it.observeSeriesWithContent(seriesId) } } override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) { - val isOnline = networkMonitor.isOnline.stateIn(scope).value - val repo = if (isOnline) onlineRepository else offlineRepository - repo.updateWatchProgress(mediaId, positionMs, durationMs) - } - - override fun setReady() { - onlineRepository.setReady() - offlineRepository.setReady() + val repository = if (networkMonitor.isOnline.first()) onlineRepository else offlineRepository + repository.updateWatchProgress(mediaId, positionMs, durationMs) } } diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/DefaultEpisodeSeriesLookup.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/DefaultEpisodeSeriesLookup.kt new file mode 100644 index 00000000..cc30b919 --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/DefaultEpisodeSeriesLookup.kt @@ -0,0 +1,14 @@ +package hu.bbara.purefin.core.data + +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DefaultEpisodeSeriesLookup @Inject constructor( + private val mediaCatalogReader: MediaCatalogReader, +) : EpisodeSeriesLookup { + override suspend fun preferenceKeyFor(mediaId: UUID): String { + return mediaCatalogReader.episodes.value[mediaId]?.seriesId?.toString() ?: mediaId.toString() + } +} diff --git a/core/player/src/main/java/hu/bbara/purefin/core/player/data/PlayerMediaRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/DefaultPlayableMediaRepository.kt similarity index 84% rename from core/player/src/main/java/hu/bbara/purefin/core/player/data/PlayerMediaRepository.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/DefaultPlayableMediaRepository.kt index 96814ca6..54ffccb9 100644 --- a/core/player/src/main/java/hu/bbara/purefin/core/player/data/PlayerMediaRepository.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/DefaultPlayableMediaRepository.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.core.player.data +package hu.bbara.purefin.core.data import android.util.Log import androidx.annotation.OptIn @@ -6,29 +6,29 @@ import androidx.core.net.toUri import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.util.UnstableApi -import dagger.hilt.android.scopes.ViewModelScoped import hu.bbara.purefin.core.data.client.JellyfinApiClient import hu.bbara.purefin.core.data.client.PlaybackDecision import hu.bbara.purefin.core.data.client.PlaybackReportContext import hu.bbara.purefin.core.data.client.playbackCustomCacheKey import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.data.session.UserSessionRepository +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.MediaSourceInfo -import java.util.UUID -import javax.inject.Inject -@ViewModelScoped -class PlayerMediaRepository @Inject constructor( +@Singleton +class DefaultPlayableMediaRepository @Inject constructor( private val jellyfinApiClient: JellyfinApiClient, - private val userSessionRepository: UserSessionRepository -) { + private val userSessionRepository: UserSessionRepository, +) : PlayableMediaRepository { - suspend fun getMediaItem(mediaId: UUID): Pair? = withContext(Dispatchers.IO) { + override suspend fun getMediaItem(mediaId: UUID): Pair? = withContext(Dispatchers.IO) { val playbackDecision = jellyfinApiClient.getPlaybackDecision(mediaId) ?: return@withContext null val baseItem = jellyfinApiClient.getItemInfo(mediaId) @@ -49,7 +49,11 @@ class PlayerMediaRepository @Inject constructor( Pair(mediaItem, resumePositionMs) } - suspend fun getNextUpMediaItems(episodeId: UUID, existingIds: Set, count: Int = 9): List = withContext(Dispatchers.IO) { + override suspend fun getNextUpMediaItems( + episodeId: UUID, + existingIds: Set, + count: Int, + ): List = withContext(Dispatchers.IO) { runCatching { val serverUrl = userSessionRepository.serverUrl.first() val episodes = jellyfinApiClient.getNextEpisodes(episodeId = episodeId, count = count) @@ -71,7 +75,7 @@ class PlayerMediaRepository @Inject constructor( ) } }.getOrElse { error -> - Log.w("PlayerMediaRepo", "Unable to load next-up items for $episodeId", error) + Log.w("PlayableMediaRepo", "Unable to load next-up items for $episodeId", error) emptyList() } } @@ -99,7 +103,7 @@ class PlayerMediaRepository @Inject constructor( playbackCustomCacheKey( mediaId = mediaId, playbackUrl = playbackDecision.url, - playMethod = playbackDecision.reportContext.playMethod + playMethod = playbackDecision.reportContext.playMethod, )?.let(builder::setCustomCacheKey) return builder.build() @@ -107,25 +111,17 @@ class PlayerMediaRepository @Inject constructor( private fun calculateResumePosition( baseItem: BaseItemDto?, - mediaSource: MediaSourceInfo + mediaSource: MediaSourceInfo, ): Long? { val userData = baseItem?.userData ?: return null - - // Get runtime in ticks val runtimeTicks = mediaSource.runTimeTicks ?: baseItem.runTimeTicks ?: 0L if (runtimeTicks == 0L) return null - // Get saved playback position from userData val playbackPositionTicks = userData.playbackPositionTicks ?: 0L if (playbackPositionTicks == 0L) return null - // Convert ticks to milliseconds val positionMs = playbackPositionTicks / 10_000 - - // Calculate percentage for threshold check val percentage = (playbackPositionTicks.toDouble() / runtimeTicks.toDouble()) * 100.0 - - // Apply thresholds: resume only if 5% ≤ progress ≤ 95% return if (percentage in 5.0..95.0) positionMs else null } diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/EpisodeSeriesLookup.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/EpisodeSeriesLookup.kt new file mode 100644 index 00000000..0a432a39 --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/EpisodeSeriesLookup.kt @@ -0,0 +1,7 @@ +package hu.bbara.purefin.core.data + +import java.util.UUID + +interface EpisodeSeriesLookup { + suspend fun preferenceKeyFor(mediaId: UUID): String +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/AppContentRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/HomeRepository.kt similarity index 90% rename from core/data/src/main/java/hu/bbara/purefin/core/data/AppContentRepository.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/HomeRepository.kt index 4fc1e86a..4ce86135 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/AppContentRepository.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/HomeRepository.kt @@ -2,11 +2,10 @@ package hu.bbara.purefin.core.data import hu.bbara.purefin.core.model.Library import hu.bbara.purefin.core.model.Media -import kotlinx.coroutines.flow.StateFlow import java.util.UUID +import kotlinx.coroutines.flow.StateFlow -interface AppContentRepository : MediaRepository { - +interface HomeRepository { val libraries: StateFlow> val suggestions: StateFlow> val continueWatching: StateFlow> diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryAppContentRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryAppContentRepository.kt index b080dede..00872bf3 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryAppContentRepository.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryAppContentRepository.kt @@ -9,27 +9,11 @@ import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.data.session.UserSessionRepository import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Library +import hu.bbara.purefin.core.model.LibraryKind import hu.bbara.purefin.core.model.Media import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Season import hu.bbara.purefin.core.model.Series -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.ImageType import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.Locale @@ -37,75 +21,114 @@ import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.jellyfin.sdk.model.api.BaseItemDto +import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.CollectionType +import org.jellyfin.sdk.model.api.ImageType @Singleton class InMemoryAppContentRepository @Inject constructor( val userSessionRepository: UserSessionRepository, val jellyfinApiClient: JellyfinApiClient, private val homeCacheDataStore: DataStore, - private val mediaRepository: CompositeMediaRepository, + private val onlineMediaRepository: InMemoryMediaRepository, private val networkMonitor: NetworkMonitor, -) : AppContentRepository { +) : HomeRepository { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private var loadJob: Job? = null + private val ensureReadyMutex = Mutex() + private var refreshJob: Job? = null + private var cacheHydrated = false + private var initialized = false - private val contentRepositoryReady : MutableStateFlow = MutableStateFlow(false) - override val ready: StateFlow = combine(contentRepositoryReady, mediaRepository.ready) { - contentRepositoryReady, mediaRepositoryReady -> - contentRepositoryReady && mediaRepositoryReady - }.stateIn(scope, SharingStarted.Eagerly, false) + private val librariesState = MutableStateFlow>(emptyList()) + override val libraries: StateFlow> = librariesState.asStateFlow() - private val _libraries: MutableStateFlow> = MutableStateFlow(emptyList()) + private val suggestionsState = MutableStateFlow>(emptyList()) + override val suggestions: StateFlow> = suggestionsState.asStateFlow() - override val libraries: StateFlow> = _libraries.asStateFlow() - private val _suggestions: MutableStateFlow> = MutableStateFlow(emptyList()) + private val continueWatchingState = MutableStateFlow>(emptyList()) + override val continueWatching: StateFlow> = continueWatchingState.asStateFlow() - override val suggestions: StateFlow> = _suggestions.asStateFlow() - private val _continueWatching: MutableStateFlow> = MutableStateFlow(emptyList()) + private val nextUpState = MutableStateFlow>(emptyList()) + override val nextUp: StateFlow> = nextUpState.asStateFlow() - override val continueWatching: StateFlow> = _continueWatching.asStateFlow() - private val _nextUp: MutableStateFlow> = MutableStateFlow(emptyList()) - - override val nextUp: StateFlow> = _nextUp.asStateFlow() - private val _latestLibraryContent: MutableStateFlow>> = MutableStateFlow(emptyMap()) - override val latestLibraryContent: StateFlow>> = _latestLibraryContent.asStateFlow() - - override val movies: StateFlow> = mediaRepository.movies - override val series: StateFlow> = mediaRepository.series - override val episodes: StateFlow> = mediaRepository.episodes - - override fun observeSeriesWithContent(seriesId: UUID): Flow { - return mediaRepository.observeSeriesWithContent(seriesId) - } - - override suspend fun updateWatchProgress( - mediaId: UUID, - positionMs: Long, - durationMs: Long - ) { - mediaRepository.updateWatchProgress(mediaId, positionMs, durationMs) - } + private val latestLibraryContentState = MutableStateFlow>>(emptyMap()) + override val latestLibraryContent: StateFlow>> = latestLibraryContentState.asStateFlow() init { scope.launch { - loadFromCache() ensureReady() } } + override suspend fun ensureReady() { + ensureReadyMutex.withLock { + hydrateCacheIfNeeded() + if (initialized) { + return + } + initialized = true + } + refreshHomeData() + } + + override suspend fun refreshHomeData() { + ensureReadyMutex.withLock { + hydrateCacheIfNeeded() + } + if (refreshJob?.isActive == true) { + refreshJob?.join() + return + } + val job = scope.launch { + runCatching { + if (!networkMonitor.isOnline.first()) { + return@runCatching + } + loadLibraries() + loadSuggestions() + loadContinueWatching() + loadNextUp() + loadLatestLibraryContent() + persistHomeCache() + }.onFailure { error -> + Log.w(TAG, "Home refresh failed; keeping cached content", error) + } + } + refreshJob = job + job.join() + } + + private suspend fun hydrateCacheIfNeeded() { + if (cacheHydrated) return + loadFromCache() + cacheHydrated = true + } + private suspend fun loadFromCache() { val cache = homeCacheDataStore.data.first() if (cache.suggestions.isNotEmpty()) { - _suggestions.value = cache.suggestions.mapNotNull { it.toMedia() } + suggestionsState.value = cache.suggestions.mapNotNull { it.toMedia() } } if (cache.continueWatching.isNotEmpty()) { - _continueWatching.value = cache.continueWatching.mapNotNull { it.toMedia() } + continueWatchingState.value = cache.continueWatching.mapNotNull { it.toMedia() } } if (cache.nextUp.isNotEmpty()) { - _nextUp.value = cache.nextUp.mapNotNull { it.toMedia() } + nextUpState.value = cache.nextUp.mapNotNull { it.toMedia() } } if (cache.latestLibraryContent.isNotEmpty()) { - _latestLibraryContent.value = cache.latestLibraryContent.mapNotNull { (key, items) -> + latestLibraryContentState.value = cache.latestLibraryContent.mapNotNull { (key, items) -> val uuid = runCatching { UUID.fromString(key) }.getOrNull() ?: return@mapNotNull null uuid to items.mapNotNull { it.toMedia() } }.toMap() @@ -114,12 +137,12 @@ class InMemoryAppContentRepository @Inject constructor( private suspend fun persistHomeCache() { val cache = HomeCache( - suggestions = _suggestions.value.map { it.toCachedItem() }, - continueWatching = _continueWatching.value.map { it.toCachedItem() }, - nextUp = _nextUp.value.map { it.toCachedItem() }, - latestLibraryContent = _latestLibraryContent.value.map { (uuid, items) -> + suggestions = suggestionsState.value.map { it.toCachedItem() }, + continueWatching = continueWatchingState.value.map { it.toCachedItem() }, + nextUp = nextUpState.value.map { it.toCachedItem() }, + latestLibraryContent = latestLibraryContentState.value.map { (uuid, items) -> uuid.toString() to items.map { it.toCachedItem() } - }.toMap() + }.toMap(), ) homeCacheDataStore.updateData { cache } } @@ -143,52 +166,26 @@ class InMemoryAppContentRepository @Inject constructor( } } - override suspend fun ensureReady() { - // check for combined ready state - if (ready.value) { - return - } - if (loadJob?.isActive == true) { - return - } - if (!contentRepositoryReady.value) { - return - } - contentRepositoryReady.value = true - loadJob?.cancel() - loadJob = scope.launch { - loadSuggestions() - loadContinueWatching() - loadNextUp() - loadLatestLibraryContent() - loadLibraries() - mediaRepository.setReady() - persistHomeCache() - } - } - suspend fun loadLibraries() { val librariesItem = runCatching { jellyfinApiClient.getLibraries() } .getOrElse { error -> Log.w(TAG, "Unable to load libraries", error) return } - //TODO add support for playlists - val filteredLibraries = - librariesItem.filter { it.collectionType == CollectionType.MOVIES || it.collectionType == CollectionType.TVSHOWS } - val emptyLibraries = filteredLibraries.map { it.toLibrary(serverUrl()) } - _libraries.value = emptyLibraries - - val filledLibraries = emptyLibraries.map { library -> - return@map loadLibrary(library) + val filteredLibraries = librariesItem.filter { + it.collectionType == CollectionType.MOVIES || it.collectionType == CollectionType.TVSHOWS } - _libraries.value = filledLibraries + val emptyLibraries = filteredLibraries.map { it.toLibrary(serverUrl()) } + librariesState.value = emptyLibraries - val movies = filledLibraries.filter { it.type == CollectionType.MOVIES }.flatMap { it.movies.orEmpty() } - mediaRepository.upsertMovies(movies) + val filledLibraries = emptyLibraries.map { loadLibrary(it) } + librariesState.value = filledLibraries - val series = filledLibraries.filter { it.type == CollectionType.TVSHOWS }.flatMap { it.series.orEmpty() } - mediaRepository.upsertSeries(series) + val movies = filledLibraries.filter { it.type == LibraryKind.MOVIES }.flatMap { it.movies.orEmpty() } + onlineMediaRepository.upsertMovies(movies) + + val series = filledLibraries.filter { it.type == LibraryKind.SERIES }.flatMap { it.series.orEmpty() } + onlineMediaRepository.upsertSeries(series) } suspend fun loadLibrary(library: Library): Library { @@ -197,71 +194,33 @@ class InMemoryAppContentRepository @Inject constructor( Log.w(TAG, "Unable to load library ${library.id}", error) return library } - when (library.type) { - CollectionType.MOVIES -> { - val movies = contentItem.map { it.toMovie(serverUrl(), library.id) } - return library.copy(movies = movies) - } - CollectionType.TVSHOWS -> { - val series = contentItem.map { it.toSeries(serverUrl(), library.id) } - return library.copy(series = series) - } - else -> throw UnsupportedOperationException("Unsupported library type: ${library.type}") + return when (library.type) { + LibraryKind.MOVIES -> library.copy( + movies = contentItem.map { it.toMovie(serverUrl(), library.id) }, + ) + LibraryKind.SERIES -> library.copy( + series = contentItem.map { it.toSeries(serverUrl(), library.id) }, + ) } } - suspend fun loadMovie(movieId: UUID): Movie { - val cachedMovie = mediaRepository.movies.value[movieId] - val movieItem = runCatching { jellyfinApiClient.getItemInfo(movieId) } - .getOrElse { error -> - Log.w(TAG, "Unable to load movie $movieId", error) - null - } - ?: return cachedMovie ?: throw RuntimeException("Movie not found") - val updatedMovie = movieItem.toMovie(serverUrl(), movieItem.parentId!!) - mediaRepository.upsertMovies(listOf(updatedMovie)) - return updatedMovie - } - - suspend fun loadSeries(seriesId: UUID): Series { - val cachedSeries = mediaRepository.series.value[seriesId] - val seriesItem = runCatching { jellyfinApiClient.getItemInfo(seriesId) } - .getOrElse { error -> - Log.w(TAG, "Unable to load series $seriesId", error) - null - } - ?: return cachedSeries ?: throw RuntimeException("Series not found") - val updatedSeries = seriesItem.toSeries(serverUrl(), seriesItem.parentId!!) - mediaRepository.upsertSeries(listOf(updatedSeries)) - return updatedSeries - } - suspend fun loadSuggestions() { val suggestionsItems = runCatching { jellyfinApiClient.getSuggestions() } .getOrElse { error -> Log.w(TAG, "Unable to load suggestions", error) return } - val items = suggestionsItems.mapNotNull { item -> + suggestionsState.value = suggestionsItems.mapNotNull { item -> when (item.type) { BaseItemKind.MOVIE -> Media.MovieMedia(movieId = item.id) - BaseItemKind.EPISODE -> Media.EpisodeMedia( - episodeId = item.id, - seriesId = item.seriesId!! - ) + BaseItemKind.EPISODE -> Media.EpisodeMedia(episodeId = item.id, seriesId = item.seriesId!!) else -> throw UnsupportedOperationException("Unsupported item type: ${item.type}") } } - _suggestions.value = items - //Load episodes, Movies are already loaded at this point suggestionsItems.forEach { item -> - when (item.type) { - BaseItemKind.EPISODE -> { - val episode = item.toEpisode(serverUrl()) - mediaRepository.upsertEpisodes(listOf(episode)) - } - else -> { /* Do nothing */ } + if (item.type == BaseItemKind.EPISODE) { + onlineMediaRepository.upsertEpisodes(listOf(item.toEpisode(serverUrl()))) } } } @@ -272,26 +231,17 @@ class InMemoryAppContentRepository @Inject constructor( Log.w(TAG, "Unable to load continue watching", error) return } - val items = continueWatchingItems.mapNotNull { item -> + continueWatchingState.value = continueWatchingItems.mapNotNull { item -> when (item.type) { BaseItemKind.MOVIE -> Media.MovieMedia(movieId = item.id) - BaseItemKind.EPISODE -> Media.EpisodeMedia( - episodeId = item.id, - seriesId = item.seriesId!! - ) + BaseItemKind.EPISODE -> Media.EpisodeMedia(episodeId = item.id, seriesId = item.seriesId!!) else -> throw UnsupportedOperationException("Unsupported item type: ${item.type}") } } - _continueWatching.value = items - //Load episodes, Movies are already loaded at this point. continueWatchingItems.forEach { item -> - when (item.type) { - BaseItemKind.EPISODE -> { - val episode = item.toEpisode(serverUrl()) - mediaRepository.upsertEpisodes(listOf(episode)) - } - else -> { /* Do nothing */ } + if (item.type == BaseItemKind.EPISODE) { + onlineMediaRepository.upsertEpisodes(listOf(item.toEpisode(serverUrl()))) } } } @@ -302,90 +252,56 @@ class InMemoryAppContentRepository @Inject constructor( Log.w(TAG, "Unable to load next up", error) return } - val items = nextUpItems.map { item -> - Media.EpisodeMedia( - episodeId = item.id, - seriesId = item.seriesId!! - ) + nextUpState.value = nextUpItems.map { item -> + Media.EpisodeMedia(episodeId = item.id, seriesId = item.seriesId!!) } - _nextUp.value = items - // Load episodes nextUpItems.forEach { item -> - val episode = item.toEpisode(serverUrl()) - mediaRepository.upsertEpisodes(listOf(episode)) + onlineMediaRepository.upsertEpisodes(listOf(item.toEpisode(serverUrl()))) } } suspend fun loadLatestLibraryContent() { - // TODO Make libraries accessible in a field or something that is not this ugly. val librariesItem = runCatching { jellyfinApiClient.getLibraries() } .getOrElse { error -> Log.w(TAG, "Unable to load latest library content", error) return } - val filterLibraries = - librariesItem.filter { it.collectionType == CollectionType.MOVIES || it.collectionType == CollectionType.TVSHOWS } - val latestLibraryContents = filterLibraries.associate { library -> + val filteredLibraries = librariesItem.filter { + it.collectionType == CollectionType.MOVIES || it.collectionType == CollectionType.TVSHOWS + } + val latestLibraryContents = filteredLibraries.associate { library -> val latestFromLibrary = runCatching { jellyfinApiClient.getLatestFromLibrary(library.id) } .getOrElse { error -> Log.w(TAG, "Unable to load latest items for library ${library.id}", error) emptyList() } library.id to when (library.collectionType) { - CollectionType.MOVIES -> { - latestFromLibrary.map { - val movie = it.toMovie(serverUrl(), library.id) - Media.MovieMedia(movieId = movie.id) - } + CollectionType.MOVIES -> latestFromLibrary.map { + val movie = it.toMovie(serverUrl(), library.id) + Media.MovieMedia(movieId = movie.id) } - CollectionType.TVSHOWS -> { - latestFromLibrary.map { - when (it.type) { - BaseItemKind.SERIES -> { - val series = it.toSeries(serverUrl(), library.id) - Media.SeriesMedia(seriesId = series.id) - } - BaseItemKind.SEASON -> { - val season = it.toSeason(serverUrl()) - Media.SeasonMedia(seasonId = season.id, seriesId = season.seriesId) - } - BaseItemKind.EPISODE -> { - val episode = it.toEpisode(serverUrl()) - Media.EpisodeMedia(episodeId = episode.id, seriesId = episode.seriesId) - } else -> throw UnsupportedOperationException("Unsupported item type: ${it.type}") + CollectionType.TVSHOWS -> latestFromLibrary.map { + when (it.type) { + BaseItemKind.SERIES -> { + val series = it.toSeries(serverUrl(), library.id) + Media.SeriesMedia(seriesId = series.id) } + BaseItemKind.SEASON -> { + val season = it.toSeason() + Media.SeasonMedia(seasonId = season.id, seriesId = season.seriesId) + } + BaseItemKind.EPISODE -> { + val episode = it.toEpisode(serverUrl()) + Media.EpisodeMedia(episodeId = episode.id, seriesId = episode.seriesId) + } + else -> throw UnsupportedOperationException("Unsupported item type: ${it.type}") } } else -> throw UnsupportedOperationException("Unsupported library type: ${library.collectionType}") } } - _latestLibraryContent.value = latestLibraryContents - - //TODO Load seasons and episodes, other types are already loaded at this point. - } - - override suspend fun refreshHomeData() { - if(loadJob?.isActive == true) { - loadJob?.join() - return - } - val job = scope.launch { - runCatching { - val isOnline = networkMonitor.isOnline.first() - if (!isOnline) return@runCatching - loadLibraries() - loadSuggestions() - loadContinueWatching() - loadNextUp() - loadLatestLibraryContent() - persistHomeCache() - }.onFailure { error -> - Log.w(TAG, "Home refresh failed; keeping cached content", error) - } - } - loadJob = job - job.join() + latestLibraryContentState.value = latestLibraryContents } private suspend fun serverUrl(): String { @@ -393,94 +309,75 @@ class InMemoryAppContentRepository @Inject constructor( } private fun BaseItemDto.toLibrary(serverUrl: String): Library { - return when (this.collectionType) { + return when (collectionType) { CollectionType.MOVIES -> Library( - id = this.id, - name = this.name!!, - posterUrl = JellyfinImageHelper.toImageUrl( - url = serverUrl, - itemId = this.id, - type = ImageType.PRIMARY - ), - type = CollectionType.MOVIES, - movies = emptyList() + id = id, + name = name!!, + posterUrl = JellyfinImageHelper.toImageUrl(url = serverUrl, itemId = id, type = ImageType.PRIMARY), + type = LibraryKind.MOVIES, + movies = emptyList(), ) CollectionType.TVSHOWS -> Library( - id = this.id, - name = this.name!!, - posterUrl = JellyfinImageHelper.toImageUrl( - url = serverUrl, - itemId = this.id, - type = ImageType.PRIMARY - ), - type = CollectionType.TVSHOWS, - series = emptyList() + id = id, + name = name!!, + posterUrl = JellyfinImageHelper.toImageUrl(url = serverUrl, itemId = id, type = ImageType.PRIMARY), + type = LibraryKind.SERIES, + series = emptyList(), ) - else -> throw UnsupportedOperationException("Unsupported library type: ${this.collectionType}") + else -> throw UnsupportedOperationException("Unsupported library type: $collectionType") } } private fun BaseItemDto.toMovie(serverUrl: String, libraryId: UUID): Movie { return Movie( - id = this.id, + id = id, libraryId = libraryId, - title = this.name ?: "Unknown title", - progress = this.userData!!.playedPercentage, - watched = this.userData!!.played, - year = this.productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(), - rating = this.officialRating - ?: "NR", - runtime = formatRuntime(this.runTimeTicks), - synopsis = this.overview ?: "No synopsis available", + title = name ?: "Unknown title", + progress = userData!!.playedPercentage, + watched = userData!!.played, + year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(), + rating = officialRating ?: "NR", + runtime = formatRuntime(runTimeTicks), + synopsis = overview ?: "No synopsis available", format = container?.uppercase() ?: "VIDEO", - imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl( - url = serverUrl, - itemId = this.id, - ), + imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(url = serverUrl, itemId = id), subtitles = "ENG", audioTrack = "ENG", - cast = emptyList() + cast = emptyList(), ) } private fun BaseItemDto.toSeries(serverUrl: String, libraryId: UUID): Series { return Series( - id = this.id, + id = id, libraryId = libraryId, - name = this.name ?: "Unknown", - synopsis = this.overview ?: "No synopsis available", - year = this.productionYear?.toString() - ?: this.premiereDate?.year?.toString().orEmpty(), - imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl( - url = serverUrl, - itemId = this.id - ), - unwatchedEpisodeCount = this.userData!!.unplayedItemCount!!, - seasonCount = this.childCount!!, + name = name ?: "Unknown", + synopsis = overview ?: "No synopsis available", + year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(), + imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(url = serverUrl, itemId = id), + unwatchedEpisodeCount = userData!!.unplayedItemCount!!, + seasonCount = childCount!!, seasons = emptyList(), - cast = emptyList() + cast = emptyList(), ) } - private fun BaseItemDto.toSeason(serverUrl: String): Season { + private fun BaseItemDto.toSeason(): Season { return Season( - id = this.id, - seriesId = this.seriesId!!, - name = this.name ?: "Unknown", - index = this.indexNumber ?: 0, - unwatchedEpisodeCount = this.userData!!.unplayedItemCount!!, - episodeCount = this.childCount!!, - episodes = emptyList() + id = id, + seriesId = seriesId!!, + name = name ?: "Unknown", + index = indexNumber ?: 0, + unwatchedEpisodeCount = userData!!.unplayedItemCount!!, + episodeCount = childCount!!, + episodes = emptyList(), ) } private fun BaseItemDto.toEpisode(serverUrl: String): Episode { val releaseDate = formatReleaseDate(premiereDate, productionYear) val imageUrlPrefix = id?.let { itemId -> - JellyfinImageHelper.toPrefixImageUrl( - url = serverUrl, - itemId = itemId - ) + JellyfinImageHelper.toPrefixImageUrl(url = serverUrl, itemId = itemId) } ?: "" return Episode( id = id, @@ -496,7 +393,7 @@ class InMemoryAppContentRepository @Inject constructor( format = container?.uppercase() ?: "VIDEO", synopsis = overview ?: "No synopsis available.", imageUrlPrefix = imageUrlPrefix, - cast = emptyList() + cast = emptyList(), ) } @@ -513,11 +410,7 @@ class InMemoryAppContentRepository @Inject constructor( val totalSeconds = ticks / 10_000_000 val hours = TimeUnit.SECONDS.toHours(totalSeconds) val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60 - return if (hours > 0) { - "${hours}h ${minutes}m" - } else { - "${minutes}m" - } + return if (hours > 0) "${hours}h ${minutes}m" else "${minutes}m" } companion object { diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryMediaRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryMediaRepository.kt index 7d4e9e41..b23b8733 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryMediaRepository.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryMediaRepository.kt @@ -7,6 +7,13 @@ import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Season import hu.bbara.purefin.core.model.Series +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale +import java.util.UUID +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -19,74 +26,41 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemDto -import org.jellyfin.sdk.model.api.ImageType -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.util.Locale -import java.util.UUID -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton @Singleton class InMemoryMediaRepository @Inject constructor( private val userSessionRepository: UserSessionRepository, private val jellyfinApiClient: JellyfinApiClient, -) : MediaRepository { +) : MediaCatalogReader, MediaProgressWriter { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - override val ready: MutableStateFlow = MutableStateFlow(false) + private val moviesState = MutableStateFlow>(emptyMap()) + override val movies: StateFlow> = moviesState.asStateFlow() - internal val _movies: MutableStateFlow> = MutableStateFlow(emptyMap()) - override val movies: StateFlow> = _movies.asStateFlow() + private val seriesState = MutableStateFlow>(emptyMap()) + override val series: StateFlow> = seriesState.asStateFlow() - internal val _series: MutableStateFlow> = MutableStateFlow(emptyMap()) - override val series: StateFlow> = _series.asStateFlow() + private val episodesState = MutableStateFlow>(emptyMap()) + override val episodes: StateFlow> = episodesState.asStateFlow() - internal val _episodes: MutableStateFlow> = MutableStateFlow(emptyMap()) - override val episodes: StateFlow> = _episodes.asStateFlow() - override fun upsertMovies(movies: List) { - _movies.update { current -> current + movies.associateBy { it.id } } + fun upsertMovies(movies: List) { + moviesState.update { current -> current + movies.associateBy { it.id } } } - override fun upsertSeries(series: List) { - _series.update { current -> current + series.associateBy { it.id } } + fun upsertSeries(series: List) { + seriesState.update { current -> current + series.associateBy { it.id } } } - override fun upsertEpisodes(episodes: List) { - _episodes.update { current -> current + episodes.associateBy { it.id } } + fun upsertEpisodes(episodes: List) { + episodesState.update { current -> current + episodes.associateBy { it.id } } } override fun observeSeriesWithContent(seriesId: UUID): Flow { scope.launch { ensureSeriesContentLoaded(seriesId) } - return _series.map { it[seriesId] } - } - - override fun setReady() { - ready.value = true - } - - private suspend fun ensureSeriesContentLoaded(seriesId: UUID) { - _series.value[seriesId]?.takeIf { it.seasons.isNotEmpty() }?.let { return } - - val series = _series.value[seriesId] ?: throw RuntimeException("Series not found") - val serverUrl = serverUrl() - - val emptySeasonsItem = jellyfinApiClient.getSeasons(seriesId) - val emptySeasons = emptySeasonsItem.map { it.toSeason(serverUrl) } - val filledSeasons = emptySeasons.map { season -> - val episodesItem = jellyfinApiClient.getEpisodesInSeason(seriesId, season.id) - val episodes = episodesItem.map { it.toEpisode(serverUrl) } - season.copy(episodes = episodes) - } - val updatedSeries = series.copy(seasons = filledSeasons) - _series.update { it + (updatedSeries.id to updatedSeries) } - - val allEpisodes = filledSeasons.flatMap { it.episodes } - _episodes.update { current -> current + allEpisodes.associateBy { it.id } } + return seriesState.map { it[seriesId] } } override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) { @@ -94,32 +68,49 @@ class InMemoryMediaRepository @Inject constructor( val progressPercent = (positionMs.toDouble() / durationMs.toDouble()) * 100.0 val watched = progressPercent >= 90.0 - if (_movies.value.containsKey(mediaId)) { - _movies.update { current -> + if (moviesState.value.containsKey(mediaId)) { + moviesState.update { current -> val movie = current[mediaId] ?: return@update current current + (mediaId to movie.copy(progress = progressPercent, watched = watched)) } return } - if (_episodes.value.containsKey(mediaId)) { - _episodes.update { current -> + if (episodesState.value.containsKey(mediaId)) { + episodesState.update { current -> val episode = current[mediaId] ?: return@update current current + (mediaId to episode.copy(progress = progressPercent, watched = watched)) } } } - private suspend fun serverUrl(): String = userSessionRepository.serverUrl.first() + private suspend fun ensureSeriesContentLoaded(seriesId: UUID) { + seriesState.value[seriesId]?.takeIf { it.seasons.isNotEmpty() }?.let { return } - private fun BaseItemDto.toSeason(serverUrl: String): Season { + val series = seriesState.value[seriesId] ?: throw RuntimeException("Series not found") + val serverUrl = userSessionRepository.serverUrl.first() + + val emptySeasons = jellyfinApiClient.getSeasons(seriesId).map { it.toSeason() } + val filledSeasons = emptySeasons.map { season -> + val episodes = jellyfinApiClient.getEpisodesInSeason(seriesId, season.id).map { it.toEpisode(serverUrl) } + season.copy(episodes = episodes) + } + + val updatedSeries = series.copy(seasons = filledSeasons) + seriesState.update { it + (updatedSeries.id to updatedSeries) } + + val allEpisodes = filledSeasons.flatMap { it.episodes } + episodesState.update { current -> current + allEpisodes.associateBy { it.id } } + } + + private fun BaseItemDto.toSeason(): Season { return Season( - id = this.id, - seriesId = this.seriesId!!, - name = this.name ?: "Unknown", - index = this.indexNumber ?: 0, - unwatchedEpisodeCount = this.userData!!.unplayedItemCount!!, - episodeCount = this.childCount!!, - episodes = emptyList() + id = id, + seriesId = seriesId!!, + name = name ?: "Unknown", + index = indexNumber ?: 0, + unwatchedEpisodeCount = userData!!.unplayedItemCount!!, + episodeCount = childCount!!, + episodes = emptyList(), ) } @@ -142,7 +133,7 @@ class InMemoryMediaRepository @Inject constructor( format = container?.uppercase() ?: "VIDEO", synopsis = overview ?: "No synopsis available.", imageUrlPrefix = imageUrlPrefix, - cast = emptyList() + cast = emptyList(), ) } diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/JellyfinPlaybackProgressReporter.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/JellyfinPlaybackProgressReporter.kt new file mode 100644 index 00000000..1518330a --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/JellyfinPlaybackProgressReporter.kt @@ -0,0 +1,29 @@ +package hu.bbara.purefin.core.data + +import hu.bbara.purefin.core.data.client.JellyfinApiClient +import hu.bbara.purefin.core.data.client.PlaybackReportContext +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class JellyfinPlaybackProgressReporter @Inject constructor( + private val jellyfinApiClient: JellyfinApiClient, +) : PlaybackProgressReporter { + override suspend fun reportPlaybackStart(itemId: UUID, positionTicks: Long, reportContext: PlaybackReportContext) { + jellyfinApiClient.reportPlaybackStart(itemId, positionTicks, reportContext) + } + + override suspend fun reportPlaybackProgress( + itemId: UUID, + positionTicks: Long, + isPaused: Boolean, + reportContext: PlaybackReportContext, + ) { + jellyfinApiClient.reportPlaybackProgress(itemId, positionTicks, isPaused, reportContext) + } + + override suspend fun reportPlaybackStopped(itemId: UUID, positionTicks: Long, reportContext: PlaybackReportContext) { + jellyfinApiClient.reportPlaybackStopped(itemId, positionTicks, reportContext) + } +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/MediaRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/MediaCatalogReader.kt similarity index 58% rename from core/data/src/main/java/hu/bbara/purefin/core/data/MediaRepository.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/MediaCatalogReader.kt index 9f836493..1f3ecd12 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/MediaRepository.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/MediaCatalogReader.kt @@ -3,23 +3,13 @@ package hu.bbara.purefin.core.data import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Series +import java.util.UUID import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import java.util.UUID -interface MediaRepository { - val ready: StateFlow +interface MediaCatalogReader { val movies: StateFlow> val series: StateFlow> val episodes: StateFlow> - fun upsertMovies(movies: List) { - } - fun upsertSeries(series: List) { - } - fun upsertEpisodes(episodes: List) { - } fun observeSeriesWithContent(seriesId: UUID): Flow - fun setReady() { - } - suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) -} \ No newline at end of file +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/MediaProgressWriter.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/MediaProgressWriter.kt new file mode 100644 index 00000000..99daf0cf --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/MediaProgressWriter.kt @@ -0,0 +1,7 @@ +package hu.bbara.purefin.core.data + +import java.util.UUID + +interface MediaProgressWriter { + suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/MediaRepositoryModule.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/MediaRepositoryModule.kt index 249bb1b7..495494e4 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/MediaRepositoryModule.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/MediaRepositoryModule.kt @@ -10,9 +10,30 @@ import dagger.hilt.components.SingletonComponent abstract class MediaRepositoryModule { @Binds - abstract fun bindAppContentRepository(impl: InMemoryAppContentRepository): AppContentRepository + abstract fun bindHomeRepository(impl: InMemoryAppContentRepository): HomeRepository @Binds - abstract fun bindMediaRepository(impl: CompositeMediaRepository): MediaRepository + abstract fun bindMediaCatalogReader(impl: CompositeMediaRepository): MediaCatalogReader + + @Binds + abstract fun bindMediaProgressWriter(impl: CompositeMediaRepository): MediaProgressWriter + + @Binds + abstract fun bindOfflineCatalogReader(impl: OfflineMediaRepository): OfflineCatalogReader + + @Binds + abstract fun bindOfflineCatalogStore(impl: RoomOfflineCatalogStore): OfflineCatalogStore + + @Binds + abstract fun bindSmartDownloadStore(impl: RoomSmartDownloadStore): SmartDownloadStore + + @Binds + abstract fun bindPlayableMediaRepository(impl: DefaultPlayableMediaRepository): PlayableMediaRepository + + @Binds + abstract fun bindEpisodeSeriesLookup(impl: DefaultEpisodeSeriesLookup): EpisodeSeriesLookup + + @Binds + abstract fun bindPlaybackProgressReporter(impl: JellyfinPlaybackProgressReporter): PlaybackProgressReporter } diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineCatalogReader.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineCatalogReader.kt new file mode 100644 index 00000000..74db5ed4 --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineCatalogReader.kt @@ -0,0 +1,13 @@ +package hu.bbara.purefin.core.data + +import hu.bbara.purefin.core.model.Episode +import hu.bbara.purefin.core.model.Movie +import hu.bbara.purefin.core.model.Series +import java.util.UUID +import kotlinx.coroutines.flow.StateFlow + +interface OfflineCatalogReader { + val movies: StateFlow> + val series: StateFlow> + val episodes: StateFlow> +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineCatalogStore.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineCatalogStore.kt new file mode 100644 index 00000000..7731db10 --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineCatalogStore.kt @@ -0,0 +1,19 @@ +package hu.bbara.purefin.core.data + +import hu.bbara.purefin.core.model.Episode +import hu.bbara.purefin.core.model.Movie +import hu.bbara.purefin.core.model.Season +import hu.bbara.purefin.core.model.Series +import java.util.UUID + +interface OfflineCatalogStore { + suspend fun saveMovies(movies: List) + suspend fun saveSeries(series: List) + suspend fun saveSeason(season: Season) + suspend fun saveEpisode(episode: Episode) + suspend fun getSeriesBasic(seriesId: UUID): Series? + suspend fun getSeason(seasonId: UUID): Season? + suspend fun deleteMovie(movieId: UUID) + suspend fun deleteEpisodeAndCleanup(episodeId: UUID) + suspend fun getEpisodesBySeries(seriesId: UUID): List +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineMediaRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineMediaRepository.kt index 0beb2374..c0b956ce 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineMediaRepository.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineMediaRepository.kt @@ -4,28 +4,21 @@ import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Series +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn -import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton -/** - * Offline media repository for managing downloaded content. - * This repository only accesses the local offline database and does not make network calls. - */ @Singleton class OfflineMediaRepository @Inject constructor( private val localDataSource: OfflineRoomMediaLocalDataSource, -) : MediaRepository { - override val ready: StateFlow = MutableStateFlow(false) - +) : OfflineCatalogReader, MediaCatalogReader, MediaProgressWriter { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) override val movies: StateFlow> = localDataSource.moviesFlow @@ -37,18 +30,6 @@ class OfflineMediaRepository @Inject constructor( override val episodes: StateFlow> = localDataSource.episodesFlow .stateIn(scope, SharingStarted.Eagerly, emptyMap()) - override fun upsertMovies(movies: List) { - TODO("Not yet implemented") - } - - override fun upsertSeries(series: List) { - TODO("Not yet implemented") - } - - override fun upsertEpisodes(episodes: List) { - TODO("Not yet implemented") - } - override fun observeSeriesWithContent(seriesId: UUID): Flow { return localDataSource.observeSeriesWithContent(seriesId) } @@ -57,12 +38,6 @@ class OfflineMediaRepository @Inject constructor( if (durationMs <= 0) return val progressPercent = (positionMs.toDouble() / durationMs.toDouble()) * 100.0 val watched = progressPercent >= 90.0 - // Write to offline database - the reactive Flows propagate changes to UI automatically localDataSource.updateWatchProgress(mediaId, progressPercent, watched) } - - override fun setReady() { - // OfflineMediaRepository works from the database. So it is ready immediately. - } - } diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/PlayableMediaRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/PlayableMediaRepository.kt new file mode 100644 index 00000000..ebf035b6 --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/PlayableMediaRepository.kt @@ -0,0 +1,13 @@ +package hu.bbara.purefin.core.data + +import androidx.media3.common.MediaItem +import java.util.UUID + +interface PlayableMediaRepository { + suspend fun getMediaItem(mediaId: UUID): Pair? + suspend fun getNextUpMediaItems( + episodeId: UUID, + existingIds: Set, + count: Int = 9, + ): List +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/PlaybackProgressReporter.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/PlaybackProgressReporter.kt new file mode 100644 index 00000000..886442de --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/PlaybackProgressReporter.kt @@ -0,0 +1,15 @@ +package hu.bbara.purefin.core.data + +import hu.bbara.purefin.core.data.client.PlaybackReportContext +import java.util.UUID + +interface PlaybackProgressReporter { + suspend fun reportPlaybackStart(itemId: UUID, positionTicks: Long, reportContext: PlaybackReportContext) + suspend fun reportPlaybackProgress( + itemId: UUID, + positionTicks: Long, + isPaused: Boolean, + reportContext: PlaybackReportContext, + ) + suspend fun reportPlaybackStopped(itemId: UUID, positionTicks: Long, reportContext: PlaybackReportContext) +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/RoomOfflineCatalogStore.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/RoomOfflineCatalogStore.kt new file mode 100644 index 00000000..c96ff8ae --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/RoomOfflineCatalogStore.kt @@ -0,0 +1,51 @@ +package hu.bbara.purefin.core.data + +import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource +import hu.bbara.purefin.core.model.Episode +import hu.bbara.purefin.core.model.Movie +import hu.bbara.purefin.core.model.Season +import hu.bbara.purefin.core.model.Series +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RoomOfflineCatalogStore @Inject constructor( + private val localDataSource: OfflineRoomMediaLocalDataSource, +) : OfflineCatalogStore { + override suspend fun saveMovies(movies: List) { + localDataSource.saveMovies(movies) + } + + override suspend fun saveSeries(series: List) { + localDataSource.saveSeries(series) + } + + override suspend fun saveSeason(season: Season) { + localDataSource.saveSeason(season) + } + + override suspend fun saveEpisode(episode: Episode) { + localDataSource.saveEpisode(episode) + } + + override suspend fun getSeriesBasic(seriesId: UUID): Series? { + return localDataSource.getSeriesBasic(seriesId) + } + + override suspend fun getSeason(seasonId: UUID): Season? { + return localDataSource.getSeason(seasonId) + } + + override suspend fun deleteMovie(movieId: UUID) { + localDataSource.deleteMovie(movieId) + } + + override suspend fun deleteEpisodeAndCleanup(episodeId: UUID) { + localDataSource.deleteEpisodeAndCleanup(episodeId) + } + + override suspend fun getEpisodesBySeries(seriesId: UUID): List { + return localDataSource.getEpisodesBySeries(seriesId) + } +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/RoomSmartDownloadStore.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/RoomSmartDownloadStore.kt new file mode 100644 index 00000000..73e49765 --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/RoomSmartDownloadStore.kt @@ -0,0 +1,29 @@ +package hu.bbara.purefin.core.data + +import hu.bbara.purefin.core.data.room.dao.SmartDownloadDao +import hu.bbara.purefin.core.data.room.entity.SmartDownloadEntity +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.flow.Flow + +@Singleton +class RoomSmartDownloadStore @Inject constructor( + private val smartDownloadDao: SmartDownloadDao, +) : SmartDownloadStore { + override suspend fun enable(seriesId: UUID) { + smartDownloadDao.insert(SmartDownloadEntity(seriesId)) + } + + override suspend fun disable(seriesId: UUID) { + smartDownloadDao.delete(seriesId) + } + + override fun observe(seriesId: UUID): Flow { + return smartDownloadDao.observe(seriesId) + } + + override suspend fun getEnabledSeriesIds(): List { + return smartDownloadDao.getAll().map { it.seriesId } + } +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/SmartDownloadStore.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/SmartDownloadStore.kt new file mode 100644 index 00000000..0652610c --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/SmartDownloadStore.kt @@ -0,0 +1,11 @@ +package hu.bbara.purefin.core.data + +import java.util.UUID +import kotlinx.coroutines.flow.Flow + +interface SmartDownloadStore { + suspend fun enable(seriesId: UUID) + suspend fun disable(seriesId: UUID) + fun observe(seriesId: UUID): Flow + suspend fun getEnabledSeriesIds(): List +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/domain/usecase/RefreshHomeDataUseCase.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/domain/usecase/RefreshHomeDataUseCase.kt index f0be3d99..6ff85a5a 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/domain/usecase/RefreshHomeDataUseCase.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/domain/usecase/RefreshHomeDataUseCase.kt @@ -1,10 +1,10 @@ package hu.bbara.purefin.core.data.domain.usecase -import hu.bbara.purefin.core.data.AppContentRepository +import hu.bbara.purefin.core.data.HomeRepository import javax.inject.Inject class RefreshHomeDataUseCase @Inject constructor( - private val repository: AppContentRepository + private val repository: HomeRepository ) { suspend operator fun invoke() { repository.refreshHomeData() diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/domain/usecase/UpdateWatchProgressUseCase.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/domain/usecase/UpdateWatchProgressUseCase.kt deleted file mode 100644 index a0790640..00000000 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/domain/usecase/UpdateWatchProgressUseCase.kt +++ /dev/null @@ -1,13 +0,0 @@ -package hu.bbara.purefin.core.data.domain.usecase - -import hu.bbara.purefin.core.data.MediaRepository -import java.util.UUID -import javax.inject.Inject - -class UpdateWatchProgressUseCase @Inject constructor( - private val repository: MediaRepository -) { - suspend operator fun invoke(mediaId: UUID, positionMs: Long, durationMs: Long) { - repository.updateWatchProgress(mediaId, positionMs, durationMs) - } -} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/download/MediaDownloadController.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/download/MediaDownloadController.kt index c20289ab..0e025c55 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/download/MediaDownloadController.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/download/MediaDownloadController.kt @@ -2,7 +2,7 @@ package hu.bbara.purefin.core.data.download import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import org.jellyfin.sdk.model.UUID +import java.util.UUID interface MediaDownloadController { fun observeActiveDownloads(): Flow> diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/image/JellyfinImageHelper.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/image/JellyfinImageHelper.kt index bd135b39..06ea34c8 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/image/JellyfinImageHelper.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/image/JellyfinImageHelper.kt @@ -1,6 +1,6 @@ package hu.bbara.purefin.core.data.image -import org.jellyfin.sdk.model.UUID +import java.util.UUID import org.jellyfin.sdk.model.api.ImageType class JellyfinImageHelper { diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/NavigationManager.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/NavigationManager.kt deleted file mode 100644 index 13f92377..00000000 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/NavigationManager.kt +++ /dev/null @@ -1,48 +0,0 @@ -package hu.bbara.purefin.core.data.navigation - -import androidx.compose.runtime.ProvidableCompositionLocal -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.staticCompositionLocalOf -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow - -sealed interface NavigationCommand { - data class Navigate(val route: Route) : NavigationCommand - data class ReplaceAll(val route: Route) : NavigationCommand - data object Pop : NavigationCommand -} - -interface NavigationManager { - val commands: SharedFlow - fun navigate(route: Route) - fun replaceAll(route: Route) - fun pop() -} - -class DefaultNavigationManager : NavigationManager { - private val _commands = - MutableSharedFlow( - extraBufferCapacity = 64, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - override val commands: SharedFlow = _commands.asSharedFlow() - - override fun navigate(route: Route) { - _commands.tryEmit(NavigationCommand.Navigate(route)) - } - - override fun replaceAll(route: Route) { - _commands.tryEmit(NavigationCommand.ReplaceAll(route)) - } - - override fun pop() { - _commands.tryEmit(NavigationCommand.Pop) - } -} - -val LocalNavigationManager: ProvidableCompositionLocal = - staticCompositionLocalOf { error("NavigationManager not provided") } - -val LocalNavigationBackStack = compositionLocalOf> { emptyList() } diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/room/offline/OfflineRoomMediaLocalDataSource.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/offline/OfflineRoomMediaLocalDataSource.kt index 1e60baba..3ee60df3 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/room/offline/OfflineRoomMediaLocalDataSource.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/offline/OfflineRoomMediaLocalDataSource.kt @@ -155,6 +155,14 @@ class OfflineRoomMediaLocalDataSource( return seasonEntity.toDomain(episodes) } + suspend fun getSeason(seasonId: UUID): Season? { + val seasonEntity = seasonDao.getById(seasonId) ?: return null + val episodes = episodeDao.getBySeasonId(seasonId).map { episodeEntity -> + episodeEntity.toDomain() + } + return seasonEntity.toDomain(episodes) + } + suspend fun getSeasons(seriesId: UUID): List { return seasonDao.getBySeriesId(seriesId).map { seasonEntity -> val episodes = episodeDao.getBySeasonId(seasonEntity.id).map { episodeEntity -> @@ -197,6 +205,10 @@ class OfflineRoomMediaLocalDataSource( } } + suspend fun deleteMovie(movieId: UUID) { + movieDao.deleteById(movieId) + } + private fun Movie.toEntity() = MovieEntity( id = id, libraryId = libraryId, diff --git a/core/data/src/test/java/hu/bbara/purefin/core/data/DefaultEpisodeSeriesLookupTest.kt b/core/data/src/test/java/hu/bbara/purefin/core/data/DefaultEpisodeSeriesLookupTest.kt new file mode 100644 index 00000000..df92d1c0 --- /dev/null +++ b/core/data/src/test/java/hu/bbara/purefin/core/data/DefaultEpisodeSeriesLookupTest.kt @@ -0,0 +1,77 @@ +package hu.bbara.purefin.core.data + +import hu.bbara.purefin.core.model.Episode +import hu.bbara.purefin.core.model.Movie +import hu.bbara.purefin.core.model.Series +import java.util.UUID +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Test + +class DefaultEpisodeSeriesLookupTest { + + @Test + fun `returns series id for known episode`() = runBlocking { + val episodeId = UUID.randomUUID() + val seriesId = UUID.randomUUID() + val lookup = DefaultEpisodeSeriesLookup( + mediaCatalogReader = FakeMediaCatalogReader( + episodes = mapOf( + episodeId to episode( + id = episodeId, + seriesId = seriesId, + ) + ) + ) + ) + + val result = lookup.preferenceKeyFor(episodeId) + + assertEquals(seriesId.toString(), result) + } + + @Test + fun `falls back to media id when episode is missing`() = runBlocking { + val mediaId = UUID.randomUUID() + val lookup = DefaultEpisodeSeriesLookup( + mediaCatalogReader = FakeMediaCatalogReader() + ) + + val result = lookup.preferenceKeyFor(mediaId) + + assertEquals(mediaId.toString(), result) + } + + private class FakeMediaCatalogReader( + movies: Map = emptyMap(), + series: Map = emptyMap(), + episodes: Map = emptyMap(), + ) : MediaCatalogReader { + override val movies: StateFlow> = MutableStateFlow(movies) + override val series: StateFlow> = MutableStateFlow(series) + override val episodes: StateFlow> = MutableStateFlow(episodes) + + override fun observeSeriesWithContent(seriesId: UUID): Flow = flowOf(series.value[seriesId]) + } + + private fun episode(id: UUID, seriesId: UUID) = Episode( + id = id, + seriesId = seriesId, + seasonId = UUID.randomUUID(), + index = 1, + title = "Episode", + synopsis = "Synopsis", + releaseDate = "2026", + rating = "PG", + runtime = "42m", + progress = null, + watched = false, + format = "VIDEO", + imageUrlPrefix = "", + cast = emptyList(), + ) +} diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index f0d118d0..3c4e61fa 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -1,30 +1,12 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) -} - -android { - namespace = "hu.bbara.purefin.core.model" - compileSdk = 36 - - defaultConfig { - minSdk = 29 - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } + alias(libs.plugins.kotlin.jvm) } kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } -} - -dependencies { - implementation(libs.jellyfin.core) + jvmToolchain(11) } diff --git a/core/model/src/main/java/hu/bbara/purefin/core/model/Library.kt b/core/model/src/main/java/hu/bbara/purefin/core/model/Library.kt index 28f36472..9f4f996a 100644 --- a/core/model/src/main/java/hu/bbara/purefin/core/model/Library.kt +++ b/core/model/src/main/java/hu/bbara/purefin/core/model/Library.kt @@ -1,12 +1,11 @@ package hu.bbara.purefin.core.model -import org.jellyfin.sdk.model.api.CollectionType import java.util.UUID data class Library( val id: UUID, val name: String, - val type: CollectionType, + val type: LibraryKind, val posterUrl: String, val series: List? = null, val movies: List? = null, @@ -14,6 +13,6 @@ data class Library( init { require(series != null || movies != null) { "Either series or movie must be provided" } require(series == null || movies == null) { "Only one of series or movie can be provided" } - require(type == CollectionType.TVSHOWS || type == CollectionType.MOVIES) { "Invalid type: $type" } + require(type == LibraryKind.SERIES || type == LibraryKind.MOVIES) { "Invalid type: $type" } } } diff --git a/core/model/src/main/java/hu/bbara/purefin/core/model/LibraryKind.kt b/core/model/src/main/java/hu/bbara/purefin/core/model/LibraryKind.kt new file mode 100644 index 00000000..ff959323 --- /dev/null +++ b/core/model/src/main/java/hu/bbara/purefin/core/model/LibraryKind.kt @@ -0,0 +1,6 @@ +package hu.bbara.purefin.core.model + +enum class LibraryKind { + MOVIES, + SERIES, +} diff --git a/core/model/src/main/java/hu/bbara/purefin/core/model/Media.kt b/core/model/src/main/java/hu/bbara/purefin/core/model/Media.kt index adc42c5a..f9eea9c2 100644 --- a/core/model/src/main/java/hu/bbara/purefin/core/model/Media.kt +++ b/core/model/src/main/java/hu/bbara/purefin/core/model/Media.kt @@ -1,14 +1,13 @@ package hu.bbara.purefin.core.model -import org.jellyfin.sdk.model.api.BaseItemKind import java.util.UUID sealed class Media( val id: UUID, - val type: BaseItemKind + val type: MediaKind, ) { - class MovieMedia(val movieId: UUID) : Media(movieId, BaseItemKind.MOVIE) - class SeriesMedia(val seriesId: UUID) : Media(seriesId, BaseItemKind.SERIES) - class SeasonMedia(val seasonId: UUID, val seriesId: UUID) : Media(seasonId, BaseItemKind.SEASON) - class EpisodeMedia(val episodeId: UUID, val seriesId: UUID) : Media(episodeId, BaseItemKind.EPISODE) + class MovieMedia(val movieId: UUID) : Media(movieId, MediaKind.MOVIE) + class SeriesMedia(val seriesId: UUID) : Media(seriesId, MediaKind.SERIES) + class SeasonMedia(val seasonId: UUID, val seriesId: UUID) : Media(seasonId, MediaKind.SEASON) + class EpisodeMedia(val episodeId: UUID, val seriesId: UUID) : Media(episodeId, MediaKind.EPISODE) } diff --git a/core/model/src/main/java/hu/bbara/purefin/core/model/MediaKind.kt b/core/model/src/main/java/hu/bbara/purefin/core/model/MediaKind.kt new file mode 100644 index 00000000..1c2495f7 --- /dev/null +++ b/core/model/src/main/java/hu/bbara/purefin/core/model/MediaKind.kt @@ -0,0 +1,8 @@ +package hu.bbara.purefin.core.model + +enum class MediaKind { + MOVIE, + SERIES, + SEASON, + EPISODE, +} diff --git a/core/model/src/main/java/hu/bbara/purefin/core/model/MediaRepositoryState.kt b/core/model/src/main/java/hu/bbara/purefin/core/model/MediaRepositoryState.kt deleted file mode 100644 index 63ec393b..00000000 --- a/core/model/src/main/java/hu/bbara/purefin/core/model/MediaRepositoryState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package hu.bbara.purefin.core.model - -sealed interface MediaRepositoryState { - data object Loading : MediaRepositoryState - data object Ready : MediaRepositoryState - data class Error(val throwable: Throwable) : MediaRepositoryState -} diff --git a/core/player/build.gradle.kts b/core/player/build.gradle.kts index 97550e92..29dd5d93 100644 --- a/core/player/build.gradle.kts +++ b/core/player/build.gradle.kts @@ -33,12 +33,12 @@ dependencies { implementation(project(":core:data")) implementation(libs.hilt) ksp(libs.hilt.compiler) + implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.medi3.exoplayer) implementation(libs.medi3.exoplayer.hls) implementation(libs.media3.datasource.okhttp) implementation(libs.datastore) implementation(libs.kotlinx.serialization.json) - implementation(libs.jellyfin.core) implementation(libs.okhttp) testImplementation(libs.junit) } diff --git a/core/player/src/main/java/hu/bbara/purefin/core/player/manager/ProgressManager.kt b/core/player/src/main/java/hu/bbara/purefin/core/player/manager/ProgressManager.kt index d0a236c4..a157123e 100644 --- a/core/player/src/main/java/hu/bbara/purefin/core/player/manager/ProgressManager.kt +++ b/core/player/src/main/java/hu/bbara/purefin/core/player/manager/ProgressManager.kt @@ -2,9 +2,9 @@ package hu.bbara.purefin.core.player.manager import android.util.Log import dagger.hilt.android.scopes.ViewModelScoped -import hu.bbara.purefin.core.data.client.JellyfinApiClient +import hu.bbara.purefin.core.data.MediaProgressWriter +import hu.bbara.purefin.core.data.PlaybackProgressReporter import hu.bbara.purefin.core.data.client.PlaybackReportContext -import hu.bbara.purefin.core.data.domain.usecase.UpdateWatchProgressUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -20,8 +20,8 @@ import javax.inject.Inject @ViewModelScoped class ProgressManager @Inject constructor( - private val jellyfinApiClient: JellyfinApiClient, - private val updateWatchProgressUseCase: UpdateWatchProgressUseCase + private val playbackProgressReporter: PlaybackProgressReporter, + private val mediaProgressWriter: MediaProgressWriter, ) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private var progressJob: Job? = null @@ -77,7 +77,7 @@ class ProgressManager @Inject constructor( report(itemId, lastPositionMs, reportContext = activePlaybackReportContext, isStop = true) scope.launch(Dispatchers.IO) { try { - updateWatchProgressUseCase(itemId, lastPositionMs, lastDurationMs) + mediaProgressWriter.updateWatchProgress(itemId, lastPositionMs, lastDurationMs) } catch (e: Exception) { Log.e("ProgressManager", "Local cache update failed", e) } @@ -102,9 +102,9 @@ class ProgressManager @Inject constructor( return@launch } when { - isStart -> jellyfinApiClient.reportPlaybackStart(itemId, ticks, reportContext) - isStop -> jellyfinApiClient.reportPlaybackStopped(itemId, ticks, reportContext) - else -> jellyfinApiClient.reportPlaybackProgress(itemId, ticks, isPaused, reportContext) + isStart -> playbackProgressReporter.reportPlaybackStart(itemId, ticks, reportContext) + isStop -> playbackProgressReporter.reportPlaybackStopped(itemId, ticks, reportContext) + else -> playbackProgressReporter.reportPlaybackProgress(itemId, ticks, isPaused, reportContext) } Log.d("ProgressManager", "${if (isStart) "Start" else if (isStop) "Stop" else "Progress"}: $itemId at ${positionMs}ms, paused=$isPaused") } catch (e: Exception) { @@ -122,9 +122,9 @@ class ProgressManager @Inject constructor( CoroutineScope(Dispatchers.IO).launch { try { activePlaybackReportContext?.let { reportContext -> - jellyfinApiClient.reportPlaybackStopped(itemId, ticks, reportContext) + playbackProgressReporter.reportPlaybackStopped(itemId, ticks, reportContext) } - updateWatchProgressUseCase(itemId, posMs, durMs) + mediaProgressWriter.updateWatchProgress(itemId, posMs, durMs) Log.d("ProgressManager", "Stop: $itemId at ${posMs}ms") } catch (e: Exception) { Log.e("ProgressManager", "Report failed", e) diff --git a/core/player/src/main/java/hu/bbara/purefin/core/player/viewmodel/PlayerViewModel.kt b/core/player/src/main/java/hu/bbara/purefin/core/player/viewmodel/PlayerViewModel.kt index 7d1b2f92..aea5fc4d 100644 --- a/core/player/src/main/java/hu/bbara/purefin/core/player/viewmodel/PlayerViewModel.kt +++ b/core/player/src/main/java/hu/bbara/purefin/core/player/viewmodel/PlayerViewModel.kt @@ -4,8 +4,8 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import hu.bbara.purefin.core.data.MediaRepository -import hu.bbara.purefin.core.player.data.PlayerMediaRepository +import hu.bbara.purefin.core.data.EpisodeSeriesLookup +import hu.bbara.purefin.core.data.PlayableMediaRepository import hu.bbara.purefin.core.player.manager.MediaContext import hu.bbara.purefin.core.player.manager.PlayerManager import hu.bbara.purefin.core.player.manager.ProgressManager @@ -25,9 +25,9 @@ import javax.inject.Inject class PlayerViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val playerManager: PlayerManager, - private val playerMediaRepository: PlayerMediaRepository, - private val mediaRepository: MediaRepository, - private val progressManager: ProgressManager + private val playableMediaRepository: PlayableMediaRepository, + private val episodeSeriesLookup: EpisodeSeriesLookup, + private val progressManager: ProgressManager, ) : ViewModel() { companion object { private const val DEFAULT_CONTROLS_AUTO_HIDE_MS = 3_500L @@ -147,12 +147,11 @@ class PlayerViewModel @Inject constructor( return } viewModelScope.launch { - val result = playerMediaRepository.getMediaItem(uuid) + val result = playableMediaRepository.getMediaItem(uuid) if (result != null) { val (mediaItem, resumePositionMs) = result - // Determine preference key: movies use their own ID, episodes use series ID - val preferenceKey = mediaRepository.episodes.value[uuid]?.seriesId?.toString() ?: id + val preferenceKey = episodeSeriesLookup.preferenceKeyFor(uuid) val mediaContext = MediaContext(mediaId = id, preferenceKey = preferenceKey) playerManager.play(mediaItem, mediaContext) @@ -175,9 +174,9 @@ class PlayerViewModel @Inject constructor( val uuid = currentMediaId.toUuidOrNull() ?: return viewModelScope.launch { val queuedIds = uiState.value.queue.map { it.id }.toSet() - val items = playerMediaRepository.getNextUpMediaItems( + val items = playableMediaRepository.getNextUpMediaItems( episodeId = uuid, - existingIds = queuedIds + existingIds = queuedIds, ) items.forEach { playerManager.addToQueue(it) } } diff --git a/feature/download/src/main/java/hu/bbara/purefin/feature/download/MediaDownloadManager.kt b/feature/download/src/main/java/hu/bbara/purefin/feature/download/MediaDownloadManager.kt index 0a4320a6..bfe82233 100644 --- a/feature/download/src/main/java/hu/bbara/purefin/feature/download/MediaDownloadManager.kt +++ b/feature/download/src/main/java/hu/bbara/purefin/feature/download/MediaDownloadManager.kt @@ -9,16 +9,13 @@ import androidx.media3.exoplayer.offline.Download import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadRequest import dagger.hilt.android.qualifiers.ApplicationContext -import hu.bbara.purefin.core.data.InMemoryMediaRepository +import hu.bbara.purefin.core.data.OfflineCatalogStore +import hu.bbara.purefin.core.data.SmartDownloadStore import hu.bbara.purefin.core.data.client.playbackCustomCacheKey import hu.bbara.purefin.core.data.client.JellyfinApiClient import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.data.download.MediaDownloadController -import hu.bbara.purefin.core.data.room.dao.MovieDao -import hu.bbara.purefin.core.data.room.dao.SmartDownloadDao -import hu.bbara.purefin.core.data.room.entity.SmartDownloadEntity -import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource import hu.bbara.purefin.core.data.session.UserSessionRepository import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Movie @@ -36,7 +33,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.jellyfin.sdk.model.UUID +import java.util.UUID import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.MediaSourceInfo @@ -51,11 +48,9 @@ class MediaDownloadManager @Inject constructor( @ApplicationContext private val context: Context, private val downloadManager: DownloadManager, private val jellyfinApiClient: JellyfinApiClient, - private val offlineDataSource: OfflineRoomMediaLocalDataSource, - private val movieDao: MovieDao, - private val smartDownloadDao: SmartDownloadDao, + private val offlineCatalogStore: OfflineCatalogStore, + private val smartDownloadStore: SmartDownloadStore, private val userSessionRepository: UserSessionRepository, - private val inMemoryMediaRepository: InMemoryMediaRepository, ) : MediaDownloadController { private val stateFlows = ConcurrentHashMap>() @@ -171,7 +166,7 @@ class MediaDownloadManager @Inject constructor( cast = emptyList() ) - offlineDataSource.saveMovies(listOf(movie)) + offlineCatalogStore.saveMovies(listOf(movie)) Log.d(TAG, "Starting download for '${movie.title}' from: $url") val request = buildDownloadRequest( @@ -193,7 +188,7 @@ class MediaDownloadManager @Inject constructor( withContext(Dispatchers.IO) { PurefinDownloadService.sendRemoveDownload(context, movieId.toString()) try { - movieDao.deleteById(movieId) + offlineCatalogStore.deleteMovie(movieId) } catch (e: Exception) { Log.e(TAG, "Failed to remove movie from offline DB", e) } @@ -231,15 +226,15 @@ class MediaDownloadManager @Inject constructor( return@withContext } - if (offlineDataSource.getSeriesBasic(series.id) == null) { - offlineDataSource.saveSeries(listOf(series)) + if (offlineCatalogStore.getSeriesBasic(series.id) == null) { + offlineCatalogStore.saveSeries(listOf(series)) } - if (offlineDataSource.getSeason(series.id, season.id) == null) { - offlineDataSource.saveSeason(season) + if (offlineCatalogStore.getSeason(season.id) == null) { + offlineCatalogStore.saveSeason(season) } - offlineDataSource.saveEpisode(episode) + offlineCatalogStore.saveEpisode(episode) Log.d(TAG, "Starting download for episode '${episode.title}' from: $url") val request = buildDownloadRequest( @@ -269,7 +264,7 @@ class MediaDownloadManager @Inject constructor( withContext(Dispatchers.IO) { PurefinDownloadService.sendRemoveDownload(context, episodeId.toString()) try { - offlineDataSource.deleteEpisodeAndCleanup(episodeId) + offlineCatalogStore.deleteEpisodeAndCleanup(episodeId) } catch (e: Exception) { Log.e(TAG, "Failed to remove episode from offline DB", e) } @@ -279,24 +274,24 @@ class MediaDownloadManager @Inject constructor( // ── Smart Download ────────────────────────────────────────────────── override suspend fun enableSmartDownload(seriesId: UUID) { - smartDownloadDao.insert(SmartDownloadEntity(seriesId)) + smartDownloadStore.enable(seriesId) syncSmartDownloadsForSeries(seriesId) } suspend fun disableSmartDownload(seriesId: UUID) { - smartDownloadDao.delete(seriesId) + smartDownloadStore.disable(seriesId) } - fun isSmartDownloadEnabled(seriesId: UUID): Flow = smartDownloadDao.observe(seriesId) + fun isSmartDownloadEnabled(seriesId: UUID): Flow = smartDownloadStore.observe(seriesId) override suspend fun syncSmartDownloads() { withContext(Dispatchers.IO) { - val enabled = smartDownloadDao.getAll() - for (entry in enabled) { + val enabledSeriesIds = smartDownloadStore.getEnabledSeriesIds() + for (seriesId in enabledSeriesIds) { try { - syncSmartDownloadsForSeries(entry.seriesId) + syncSmartDownloadsForSeries(seriesId) } catch (e: Exception) { - Log.e(TAG, "Smart download sync failed for series ${entry.seriesId}", e) + Log.e(TAG, "Smart download sync failed for series $seriesId", e) } } } @@ -307,7 +302,7 @@ class MediaDownloadManager @Inject constructor( val serverUrl = userSessionRepository.serverUrl.first().trim() // 1. Get currently downloaded episodes for this series - val downloadedEpisodes = offlineDataSource.getEpisodesBySeries(seriesId) + val downloadedEpisodes = offlineCatalogStore.getEpisodesBySeries(seriesId) // 2. Check watched status from server and delete watched downloads val unwatchedDownloaded = mutableListOf() diff --git a/feature/shared/build.gradle.kts b/feature/shared/build.gradle.kts index 2e833269..af476056 100644 --- a/feature/shared/build.gradle.kts +++ b/feature/shared/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) alias(libs.plugins.hilt) alias(libs.plugins.ksp) } @@ -33,5 +34,9 @@ dependencies { implementation(libs.hilt) ksp(libs.hilt.compiler) implementation(libs.jellyfin.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.navigation3.runtime) + testImplementation(libs.junit) } diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/episode/EpisodeScreenViewModel.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/episode/EpisodeScreenViewModel.kt index 9aceab9b..d12fa2e8 100644 --- a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/episode/EpisodeScreenViewModel.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/episode/EpisodeScreenViewModel.kt @@ -3,13 +3,14 @@ package hu.bbara.purefin.feature.shared.content.episode import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import hu.bbara.purefin.core.data.AppContentRepository -import hu.bbara.purefin.core.data.navigation.NavigationManager -import hu.bbara.purefin.core.data.navigation.Route -import hu.bbara.purefin.core.data.navigation.SeriesDto +import hu.bbara.purefin.core.data.MediaCatalogReader import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.data.download.MediaDownloadController import hu.bbara.purefin.core.model.Episode +import hu.bbara.purefin.feature.shared.navigation.NavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route +import hu.bbara.purefin.feature.shared.navigation.SeriesDto +import java.util.UUID import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -17,12 +18,11 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.UUID import javax.inject.Inject @HiltViewModel class EpisodeScreenViewModel @Inject constructor( - private val appContentRepository: AppContentRepository, + private val mediaCatalogReader: MediaCatalogReader, private val navigationManager: NavigationManager, private val mediaDownloadManager: MediaDownloadController, ): ViewModel() { @@ -32,14 +32,14 @@ class EpisodeScreenViewModel @Inject constructor( val episode: StateFlow = combine( _episodeId, - appContentRepository.episodes + mediaCatalogReader.episodes ) { id, episodesMap -> id?.let { episodesMap[it] } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) val seriesTitle: StateFlow = combine( _seriesId, - appContentRepository.series + mediaCatalogReader.series ) { seriesId, seriesMap -> seriesId?.let { id -> seriesMap[id]?.name } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/movie/MovieScreenViewModel.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/movie/MovieScreenViewModel.kt index 0d2883ea..489539d2 100644 --- a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/movie/MovieScreenViewModel.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/movie/MovieScreenViewModel.kt @@ -3,12 +3,13 @@ package hu.bbara.purefin.feature.shared.content.movie import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import hu.bbara.purefin.core.data.MediaRepository -import hu.bbara.purefin.core.data.navigation.NavigationManager -import hu.bbara.purefin.core.data.navigation.Route +import hu.bbara.purefin.core.data.MediaCatalogReader import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.data.download.MediaDownloadController import hu.bbara.purefin.core.model.Movie +import hu.bbara.purefin.feature.shared.navigation.NavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route +import java.util.UUID import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -16,21 +17,20 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.UUID import javax.inject.Inject @HiltViewModel class MovieScreenViewModel @Inject constructor( - private val mediaRepository: MediaRepository, + private val mediaCatalogReader: MediaCatalogReader, private val navigationManager: NavigationManager, - private val mediaDownloadManager: MediaDownloadController + private val mediaDownloadManager: MediaDownloadController, ): ViewModel() { private val _movieId = MutableStateFlow(null) val movie: StateFlow = combine( _movieId, - mediaRepository.movies + mediaCatalogReader.movies ) { movieId, movies -> movieId?.let { movies[it] } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/series/SeriesViewModel.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/series/SeriesViewModel.kt index 7bad3e65..b612de92 100644 --- a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/series/SeriesViewModel.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/content/series/SeriesViewModel.kt @@ -3,14 +3,15 @@ package hu.bbara.purefin.feature.shared.content.series import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import hu.bbara.purefin.core.data.MediaRepository -import hu.bbara.purefin.core.data.navigation.EpisodeDto -import hu.bbara.purefin.core.data.navigation.NavigationManager -import hu.bbara.purefin.core.data.navigation.Route +import hu.bbara.purefin.core.data.MediaCatalogReader import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.data.download.MediaDownloadController import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Series +import hu.bbara.purefin.feature.shared.navigation.EpisodeDto +import hu.bbara.purefin.feature.shared.navigation.NavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route +import java.util.UUID import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -20,12 +21,11 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.UUID import javax.inject.Inject @HiltViewModel class SeriesViewModel @Inject constructor( - private val mediaRepository: MediaRepository, + private val mediaCatalogReader: MediaCatalogReader, private val navigationManager: NavigationManager, private val mediaDownloadManager: MediaDownloadController, ) : ViewModel() { @@ -35,7 +35,7 @@ class SeriesViewModel @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) val series: StateFlow = _seriesId .flatMapLatest { id -> - if (id != null) mediaRepository.observeSeriesWithContent(id) else flowOf(null) + if (id != null) mediaCatalogReader.observeSeriesWithContent(id) else flowOf(null) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/download/DownloadsViewModel.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/download/DownloadsViewModel.kt index 61e369f8..9d6bd8dc 100644 --- a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/download/DownloadsViewModel.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/download/DownloadsViewModel.kt @@ -3,28 +3,28 @@ package hu.bbara.purefin.feature.shared.download import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import hu.bbara.purefin.core.data.OfflineMediaRepository +import hu.bbara.purefin.core.data.OfflineCatalogReader import hu.bbara.purefin.core.data.download.MediaDownloadController import hu.bbara.purefin.core.data.image.JellyfinImageHelper -import hu.bbara.purefin.core.data.navigation.MovieDto -import hu.bbara.purefin.core.data.navigation.NavigationManager -import hu.bbara.purefin.core.data.navigation.Route -import hu.bbara.purefin.core.data.navigation.SeriesDto +import hu.bbara.purefin.core.model.MediaKind +import hu.bbara.purefin.feature.shared.navigation.MovieDto +import hu.bbara.purefin.feature.shared.navigation.NavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route +import hu.bbara.purefin.feature.shared.navigation.SeriesDto import hu.bbara.purefin.feature.shared.home.PosterItem +import java.util.UUID import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.ImageType import javax.inject.Inject @HiltViewModel class DownloadsViewModel @Inject constructor( - private val offlineMediaRepository: OfflineMediaRepository, + private val offlineCatalogReader: OfflineCatalogReader, private val navigationManager: NavigationManager, - private val downloadManager: MediaDownloadController + private val downloadManager: MediaDownloadController, ) : ViewModel() { fun onMovieSelected(movieId: UUID) { @@ -47,22 +47,22 @@ class DownloadsViewModel @Inject constructor( /** Items that are fully downloaded and not currently in progress. */ val downloads = combine( - offlineMediaRepository.movies, - offlineMediaRepository.series, + offlineCatalogReader.movies, + offlineCatalogReader.series, activeDownloadsMap ) { movies, series, inProgress -> movies.values .filter { it.id.toString() !in inProgress } - .map { PosterItem(type = BaseItemKind.MOVIE, movie = it) } + - series.values.map { PosterItem(type = BaseItemKind.SERIES, series = it) } + .map { PosterItem(type = MediaKind.MOVIE, movie = it) } + + series.values.map { PosterItem(type = MediaKind.SERIES, series = it) } } /** Items currently being downloaded with their progress. */ val activeDownloads = combine( activeDownloadsMap, - offlineMediaRepository.movies, - offlineMediaRepository.episodes, - offlineMediaRepository.series + offlineCatalogReader.movies, + offlineCatalogReader.episodes, + offlineCatalogReader.series ) { inProgress, movies, episodes, seriesMap -> inProgress.mapNotNull { (contentId, progress) -> val id = try { UUID.fromString(contentId) } catch (e: Exception) { return@mapNotNull null } @@ -97,7 +97,7 @@ class DownloadsViewModel @Inject constructor( } catch (e: Exception) { return@launch } - if (offlineMediaRepository.episodes.value.containsKey(id)) { + if (offlineCatalogReader.episodes.value.containsKey(id)) { downloadManager.cancelEpisodeDownload(id) } else { downloadManager.cancelDownload(id) diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/AppViewModel.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/AppViewModel.kt index ad63f53b..ae0f4db6 100644 --- a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/AppViewModel.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/AppViewModel.kt @@ -3,17 +3,21 @@ package hu.bbara.purefin.feature.shared.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import hu.bbara.purefin.core.data.AppContentRepository +import hu.bbara.purefin.core.data.HomeRepository +import hu.bbara.purefin.core.data.MediaCatalogReader import hu.bbara.purefin.core.data.domain.usecase.RefreshHomeDataUseCase -import hu.bbara.purefin.core.data.navigation.EpisodeDto -import hu.bbara.purefin.core.data.navigation.LibraryDto -import hu.bbara.purefin.core.data.navigation.MovieDto -import hu.bbara.purefin.core.data.navigation.NavigationManager -import hu.bbara.purefin.core.data.navigation.Route -import hu.bbara.purefin.core.data.navigation.SeriesDto -import hu.bbara.purefin.core.data.session.UserSessionRepository import hu.bbara.purefin.core.data.download.MediaDownloadController +import hu.bbara.purefin.core.data.session.UserSessionRepository +import hu.bbara.purefin.core.model.LibraryKind import hu.bbara.purefin.core.model.Media +import hu.bbara.purefin.core.model.MediaKind +import hu.bbara.purefin.feature.shared.navigation.EpisodeDto +import hu.bbara.purefin.feature.shared.navigation.LibraryDto +import hu.bbara.purefin.feature.shared.navigation.MovieDto +import hu.bbara.purefin.feature.shared.navigation.NavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route +import hu.bbara.purefin.feature.shared.navigation.SeriesDto +import java.util.UUID import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -22,14 +26,12 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CollectionType import javax.inject.Inject @HiltViewModel class AppViewModel @Inject constructor( - private val appContentRepository: AppContentRepository, + private val homeRepository: HomeRepository, + private val mediaCatalogReader: MediaCatalogReader, private val userSessionRepository: UserSessionRepository, private val navigationManager: NavigationManager, private val refreshHomeDataUseCase: RefreshHomeDataUseCase, @@ -46,7 +48,7 @@ class AppViewModel @Inject constructor( "" ) - val libraries = appContentRepository.libraries.map { libraries -> + val libraries = homeRepository.libraries.map { libraries -> libraries.map { LibraryItem( id = it.id, @@ -54,19 +56,18 @@ class AppViewModel @Inject constructor( type = it.type, posterUrl = it.posterUrl, isEmpty = when(it.type) { - CollectionType.MOVIES -> appContentRepository.movies.value.isEmpty() - CollectionType.TVSHOWS -> appContentRepository.series.value.isEmpty() - else -> true + LibraryKind.MOVIES -> mediaCatalogReader.movies.value.isEmpty() + LibraryKind.SERIES -> mediaCatalogReader.series.value.isEmpty() } ) } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) val suggestions = combine( - appContentRepository.suggestions, - appContentRepository.movies, - appContentRepository.series, - appContentRepository.episodes + homeRepository.suggestions, + mediaCatalogReader.movies, + mediaCatalogReader.series, + mediaCatalogReader.episodes ) { list, moviesMap, seriesMap, episodesMap -> list.mapNotNull { media -> when (media) { @@ -89,17 +90,17 @@ class AppViewModel @Inject constructor( ) val continueWatching = combine( - appContentRepository.continueWatching, - appContentRepository.movies, - appContentRepository.episodes + homeRepository.continueWatching, + mediaCatalogReader.movies, + mediaCatalogReader.episodes ) { list, moviesMap, episodesMap -> list.mapNotNull { media -> when (media) { is Media.MovieMedia -> moviesMap[media.movieId]?.let { - ContinueWatchingItem(type = BaseItemKind.MOVIE, movie = it) + ContinueWatchingItem(type = MediaKind.MOVIE, movie = it) } is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let { - ContinueWatchingItem(type = BaseItemKind.EPISODE, episode = it) + ContinueWatchingItem(type = MediaKind.EPISODE, episode = it) } else -> null } @@ -111,8 +112,8 @@ class AppViewModel @Inject constructor( ) val nextUp = combine( - appContentRepository.nextUp, - appContentRepository.episodes + homeRepository.nextUp, + mediaCatalogReader.episodes ) { list, episodesMap -> list.mapNotNull { media -> when (media) { @@ -129,27 +130,26 @@ class AppViewModel @Inject constructor( ) val latestLibraryContent = combine( - appContentRepository.latestLibraryContent, - appContentRepository.movies, - appContentRepository.series, - appContentRepository.episodes + homeRepository.latestLibraryContent, + mediaCatalogReader.movies, + mediaCatalogReader.series, + mediaCatalogReader.episodes ) { libraryMap, moviesMap, seriesMap, episodesMap -> libraryMap.mapValues { (_, items) -> items.mapNotNull { media -> when (media) { is Media.MovieMedia -> moviesMap[media.movieId]?.let { - PosterItem(type = BaseItemKind.MOVIE, movie = it) + PosterItem(type = MediaKind.MOVIE, movie = it) } is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let { - PosterItem(type = BaseItemKind.EPISODE, episode = it) + PosterItem(type = MediaKind.EPISODE, episode = it) } is Media.SeriesMedia -> seriesMap[media.seriesId]?.let { - PosterItem(type = BaseItemKind.SERIES, series = it) + PosterItem(type = MediaKind.SERIES, series = it) } is Media.SeasonMedia -> seriesMap[media.seriesId]?.let { - PosterItem(type = BaseItemKind.SERIES, series = it) + PosterItem(type = MediaKind.SERIES, series = it) } - else -> null } }.distinctBy { it.id } } @@ -242,5 +242,4 @@ class AppViewModel @Inject constructor( userSessionRepository.setLoggedIn(false) } } - } diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomeModels.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomeModels.kt index 9cf20d34..d74b8885 100644 --- a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomeModels.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomeModels.kt @@ -4,9 +4,9 @@ import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Series -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CollectionType +import java.util.UUID +import hu.bbara.purefin.core.model.MediaKind +import hu.bbara.purefin.core.model.LibraryKind import org.jellyfin.sdk.model.api.ImageType @@ -20,7 +20,7 @@ sealed interface SuggestedItem { val imageUrl: String val ctaLabel: String val progress: Float? - val type: BaseItemKind + val type: MediaKind } data class SuggestedEpisode ( @@ -34,7 +34,7 @@ data class SuggestedEpisode ( .filter { it.isNotBlank() }, override val ctaLabel: String = "Open", override val progress: Float? = episode.progress?.toFloat(), - override val type: BaseItemKind = BaseItemKind.EPISODE, + override val type: MediaKind = MediaKind.EPISODE, override val id: UUID = episode.id, override val title: String = episode.title, override val description: String = episode.synopsis, @@ -57,7 +57,7 @@ data class SuggestedSeries ( listOf(series.year, "${series.seasonCount} seasons").filter { it.isNotBlank() }, override val ctaLabel: String = "Open", override val progress: Float? = null, - override val type: BaseItemKind = BaseItemKind.SERIES, + override val type: MediaKind = MediaKind.SERIES, override val id: UUID = series.id, override val title: String = series.name, override val description: String = series.synopsis, @@ -78,7 +78,7 @@ data class SuggestedMovie ( .filter { it.isNotBlank() }, override val ctaLabel: String = "Open", override val progress: Float? = movie.progress?.toFloat(), - override val type: BaseItemKind = BaseItemKind.MOVIE, + override val type: MediaKind = MediaKind.MOVIE, override val id: UUID = movie.id, override val title: String = movie.title, override val description: String = movie.synopsis, @@ -94,48 +94,48 @@ sealed interface FocusableItem { val secondaryText: String val description: String val id: UUID - val type: BaseItemKind + val type: MediaKind } data class ContinueWatchingItem( - override val type: BaseItemKind, + override val type: MediaKind, val movie: Movie? = null, val episode: Episode? = null, ) : FocusableItem { override val id: UUID = when (type) { - BaseItemKind.MOVIE -> movie!!.id - BaseItemKind.EPISODE -> episode!!.id + MediaKind.MOVIE -> movie!!.id + MediaKind.EPISODE -> episode!!.id else -> throw UnsupportedOperationException("Unsupported item type: $type") } override val primaryText: String = when (type) { - BaseItemKind.MOVIE -> movie!!.title - BaseItemKind.EPISODE -> episode!!.title + MediaKind.MOVIE -> movie!!.title + MediaKind.EPISODE -> episode!!.title else -> throw UnsupportedOperationException("Unsupported item type: $type") } override val secondaryText: String = when (type) { - BaseItemKind.MOVIE -> movie!!.year - BaseItemKind.EPISODE -> episode!!.releaseDate + MediaKind.MOVIE -> movie!!.year + MediaKind.EPISODE -> episode!!.releaseDate else -> throw UnsupportedOperationException("Unsupported item type: $type") } override val imageUrl: String = when (type) { - BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl( + MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl( prefixImageUrl = movie!!.imageUrlPrefix, imageType = ImageType.PRIMARY ) - BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl( + MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl( prefixImageUrl = episode!!.imageUrlPrefix, imageType = ImageType.PRIMARY ) else -> throw IllegalArgumentException("Invalid type: $type") } override val description: String = when (type) { - BaseItemKind.MOVIE -> movie!!.synopsis - BaseItemKind.EPISODE -> episode!!.synopsis + MediaKind.MOVIE -> movie!!.synopsis + MediaKind.EPISODE -> episode!!.synopsis else -> throw UnsupportedOperationException("Unsupported item type: $type") } val progress: Double = when (type) { - BaseItemKind.MOVIE -> movie!!.progress ?: 0.0 - BaseItemKind.EPISODE -> episode!!.progress ?: 0.0 + MediaKind.MOVIE -> movie!!.progress ?: 0.0 + MediaKind.EPISODE -> episode!!.progress ?: 0.0 else -> throw UnsupportedOperationException("Unsupported item type: $type") } } @@ -144,8 +144,8 @@ data class NextUpItem( val episode: Episode ) : FocusableItem { override val id: UUID = episode.id - override val type: BaseItemKind - get() = BaseItemKind.EPISODE + override val type: MediaKind + get() = MediaKind.EPISODE override val imageUrl: String get() = JellyfinImageHelper.finishImageUrl( prefixImageUrl = episode.imageUrlPrefix, @@ -160,39 +160,39 @@ data class NextUpItem( data class LibraryItem( val id: UUID, val name: String, - val type: CollectionType, + val type: LibraryKind, val posterUrl: String, val isEmpty: Boolean ) data class PosterItem( - override val type: BaseItemKind, + override val type: MediaKind, val movie: Movie? = null, val series: Series? = null, val episode: Episode? = null ) : FocusableItem { override val id: UUID = when (type) { - BaseItemKind.MOVIE -> movie!!.id - BaseItemKind.EPISODE -> episode!!.id - BaseItemKind.SERIES -> series!!.id + MediaKind.MOVIE -> movie!!.id + MediaKind.EPISODE -> episode!!.id + MediaKind.SERIES -> series!!.id else -> throw IllegalArgumentException("Invalid type: $type") } val title: String = when (type) { - BaseItemKind.MOVIE -> movie!!.title - BaseItemKind.EPISODE -> episode!!.title - BaseItemKind.SERIES -> series!!.name + MediaKind.MOVIE -> movie!!.title + MediaKind.EPISODE -> episode!!.title + MediaKind.SERIES -> series!!.name else -> throw IllegalArgumentException("Invalid type: $type") } override val imageUrl: String = when (type) { - BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl( + MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl( prefixImageUrl = movie!!.imageUrlPrefix, imageType = ImageType.PRIMARY ) - BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl( + MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl( prefixImageUrl = episode!!.imageUrlPrefix, imageType = ImageType.PRIMARY ) - BaseItemKind.SERIES -> JellyfinImageHelper.finishImageUrl( + MediaKind.SERIES -> JellyfinImageHelper.finishImageUrl( prefixImageUrl = series!!.imageUrlPrefix, imageType = ImageType.PRIMARY ) @@ -200,29 +200,29 @@ data class PosterItem( } override val primaryText: String get() = when (type) { - BaseItemKind.MOVIE -> movie!!.title - BaseItemKind.EPISODE -> episode!!.title - BaseItemKind.SERIES -> series!!.name + MediaKind.MOVIE -> movie!!.title + MediaKind.EPISODE -> episode!!.title + MediaKind.SERIES -> series!!.name else -> throw IllegalArgumentException("Invalid type: $type") } override val secondaryText: String get() = when (type) { - BaseItemKind.MOVIE -> movie!!.year - BaseItemKind.EPISODE -> episode!!.releaseDate - BaseItemKind.SERIES -> series!!.year + MediaKind.MOVIE -> movie!!.year + MediaKind.EPISODE -> episode!!.releaseDate + MediaKind.SERIES -> series!!.year else -> throw IllegalArgumentException("Invalid type: $type") } override val description: String get() = when (type) { - BaseItemKind.MOVIE -> movie!!.synopsis - BaseItemKind.EPISODE -> episode!!.synopsis - BaseItemKind.SERIES -> series!!.synopsis + MediaKind.MOVIE -> movie!!.synopsis + MediaKind.EPISODE -> episode!!.synopsis + MediaKind.SERIES -> series!!.synopsis else -> throw IllegalArgumentException("Invalid type: $type") } fun watched() = when (type) { - BaseItemKind.MOVIE -> movie!!.watched - BaseItemKind.EPISODE -> episode!!.watched + MediaKind.MOVIE -> movie!!.watched + MediaKind.EPISODE -> episode!!.watched else -> throw IllegalArgumentException("Invalid type: $type") } } diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/library/LibraryViewModel.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/library/LibraryViewModel.kt index 335e1a62..05711bb3 100644 --- a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/library/LibraryViewModel.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/library/LibraryViewModel.kt @@ -3,50 +3,49 @@ package hu.bbara.purefin.feature.shared.library import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import hu.bbara.purefin.core.data.HomeRepository +import hu.bbara.purefin.core.model.LibraryKind +import hu.bbara.purefin.core.model.MediaKind import hu.bbara.purefin.feature.shared.home.PosterItem -import hu.bbara.purefin.core.data.AppContentRepository -import hu.bbara.purefin.core.data.navigation.MovieDto -import hu.bbara.purefin.core.data.navigation.NavigationManager -import hu.bbara.purefin.core.data.navigation.Route -import hu.bbara.purefin.core.data.navigation.SeriesDto +import hu.bbara.purefin.feature.shared.navigation.MovieDto +import hu.bbara.purefin.feature.shared.navigation.NavigationManager +import hu.bbara.purefin.feature.shared.navigation.Route +import hu.bbara.purefin.feature.shared.navigation.SeriesDto +import java.util.UUID import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CollectionType import javax.inject.Inject @HiltViewModel class LibraryViewModel @Inject constructor( - private val appContentRepository: AppContentRepository, - private val navigationManager: NavigationManager + private val homeRepository: HomeRepository, + private val navigationManager: NavigationManager, ) : ViewModel() { private val selectedLibrary = MutableStateFlow(null) - val contents: StateFlow> = combine(selectedLibrary, appContentRepository.libraries) { + val contents: StateFlow> = combine(selectedLibrary, homeRepository.libraries) { libraryId, libraries -> if (libraryId == null) { return@combine emptyList() } val library = libraries.find { it.id == libraryId } ?: return@combine emptyList() when (library.type) { - CollectionType.TVSHOWS -> library.series!!.map { series -> - PosterItem(type = BaseItemKind.SERIES, series = series) + LibraryKind.SERIES -> library.series!!.map { series -> + PosterItem(type = MediaKind.SERIES, series = series) } - CollectionType.MOVIES -> library.movies!!.map { movie -> - PosterItem(type = BaseItemKind.MOVIE, movie = movie) + LibraryKind.MOVIES -> library.movies!!.map { movie -> + PosterItem(type = MediaKind.MOVIE, movie = movie) } - else -> emptyList() } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) init { - viewModelScope.launch { appContentRepository.ensureReady() } + viewModelScope.launch { homeRepository.ensureReady() } } fun onMovieSelected(movieId: UUID) { @@ -72,8 +71,6 @@ class LibraryViewModel @Inject constructor( } fun selectLibrary(libraryId: UUID) { - viewModelScope.launch { - selectedLibrary.value = libraryId - } + selectedLibrary.value = libraryId } } diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/EpisodeDto.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/EpisodeDto.kt similarity index 87% rename from core/data/src/main/java/hu/bbara/purefin/core/data/navigation/EpisodeDto.kt rename to feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/EpisodeDto.kt index 6991a98b..32fa1473 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/EpisodeDto.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/EpisodeDto.kt @@ -1,8 +1,8 @@ -package hu.bbara.purefin.core.data.navigation +package hu.bbara.purefin.feature.shared.navigation +import java.util.UUID import kotlinx.serialization.Serializable import org.jellyfin.sdk.model.serializer.UUIDSerializer -import java.util.UUID @Serializable data class EpisodeDto( diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/LibraryDto.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/LibraryDto.kt similarity index 58% rename from core/data/src/main/java/hu/bbara/purefin/core/data/navigation/LibraryDto.kt rename to feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/LibraryDto.kt index 9d339441..13dbbb85 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/LibraryDto.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/LibraryDto.kt @@ -1,12 +1,12 @@ -package hu.bbara.purefin.core.data.navigation +package hu.bbara.purefin.feature.shared.navigation +import java.util.UUID import kotlinx.serialization.Serializable -import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.serializer.UUIDSerializer @Serializable -data class LibraryDto ( +data class LibraryDto( @Serializable(with = UUIDSerializer::class) val id: UUID, - val name: String -) \ No newline at end of file + val name: String, +) diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/MovieDto.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/MovieDto.kt similarity index 75% rename from core/data/src/main/java/hu/bbara/purefin/core/data/navigation/MovieDto.kt rename to feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/MovieDto.kt index 9631879a..4498b4a1 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/MovieDto.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/MovieDto.kt @@ -1,11 +1,11 @@ -package hu.bbara.purefin.core.data.navigation +package hu.bbara.purefin.feature.shared.navigation +import java.util.UUID import kotlinx.serialization.Serializable import org.jellyfin.sdk.model.serializer.UUIDSerializer -import java.util.UUID @Serializable data class MovieDto( @Serializable(with = UUIDSerializer::class) - val id: UUID + val id: UUID, ) diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/NavigationManager.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/NavigationManager.kt new file mode 100644 index 00000000..17ac3c0d --- /dev/null +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/NavigationManager.kt @@ -0,0 +1,40 @@ +package hu.bbara.purefin.feature.shared.navigation + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +sealed interface NavigationCommand { + data class Navigate(val route: Route) : NavigationCommand + data class ReplaceAll(val route: Route) : NavigationCommand + data object Pop : NavigationCommand +} + +interface NavigationManager { + val commands: SharedFlow + fun navigate(route: Route) + fun replaceAll(route: Route) + fun pop() +} + +class DefaultNavigationManager : NavigationManager { + private val commandsFlow = MutableSharedFlow( + extraBufferCapacity = 64, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + override val commands: SharedFlow = commandsFlow.asSharedFlow() + + override fun navigate(route: Route) { + commandsFlow.tryEmit(NavigationCommand.Navigate(route)) + } + + override fun replaceAll(route: Route) { + commandsFlow.tryEmit(NavigationCommand.ReplaceAll(route)) + } + + override fun pop() { + commandsFlow.tryEmit(NavigationCommand.Pop) + } +} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/NavigationManagerModule.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/NavigationManagerModule.kt similarity index 87% rename from core/data/src/main/java/hu/bbara/purefin/core/data/navigation/NavigationManagerModule.kt rename to feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/NavigationManagerModule.kt index e143a89b..2ac0bd52 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/NavigationManagerModule.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/NavigationManagerModule.kt @@ -1,4 +1,4 @@ -package hu.bbara.purefin.core.data.navigation +package hu.bbara.purefin.feature.shared.navigation import dagger.Module import dagger.Provides diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/Route.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/Route.kt similarity index 51% rename from core/data/src/main/java/hu/bbara/purefin/core/data/navigation/Route.kt rename to feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/Route.kt index d0690751..16e13123 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/Route.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/Route.kt @@ -1,23 +1,24 @@ -package hu.bbara.purefin.core.data.navigation +package hu.bbara.purefin.feature.shared.navigation import androidx.navigation3.runtime.NavKey import kotlinx.serialization.Serializable +@Serializable sealed interface Route : NavKey { @Serializable - data object Home: Route + data object Home : Route @Serializable - data class MovieRoute(val item : MovieDto) : Route + data class MovieRoute(val item: MovieDto) : Route @Serializable - data class SeriesRoute(val item : SeriesDto) : Route + data class SeriesRoute(val item: SeriesDto) : Route @Serializable - data class EpisodeRoute(val item : EpisodeDto) : Route + data class EpisodeRoute(val item: EpisodeDto) : Route @Serializable - data class LibraryRoute(val library : LibraryDto) : Route + data class LibraryRoute(val library: LibraryDto) : Route @Serializable data object LoginRoute : Route diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/SeriesDto.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/SeriesDto.kt similarity index 75% rename from core/data/src/main/java/hu/bbara/purefin/core/data/navigation/SeriesDto.kt rename to feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/SeriesDto.kt index 9a5fa508..6c6a72a7 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/navigation/SeriesDto.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/navigation/SeriesDto.kt @@ -1,11 +1,11 @@ -package hu.bbara.purefin.core.data.navigation +package hu.bbara.purefin.feature.shared.navigation +import java.util.UUID import kotlinx.serialization.Serializable import org.jellyfin.sdk.model.serializer.UUIDSerializer -import java.util.UUID @Serializable data class SeriesDto( @Serializable(with = UUIDSerializer::class) - val id: UUID + val id: UUID, ) diff --git a/core/model/src/main/java/hu/bbara/purefin/core/model/SearchResult.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/search/SearchResult.kt similarity index 51% rename from core/model/src/main/java/hu/bbara/purefin/core/model/SearchResult.kt rename to feature/shared/src/main/java/hu/bbara/purefin/feature/shared/search/SearchResult.kt index efeabef0..cc1144dd 100644 --- a/core/model/src/main/java/hu/bbara/purefin/core/model/SearchResult.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/search/SearchResult.kt @@ -1,30 +1,32 @@ -package hu.bbara.purefin.core.model +package hu.bbara.purefin.feature.shared.search -import org.jellyfin.sdk.model.UUID -import org.jellyfin.sdk.model.api.BaseItemKind +import hu.bbara.purefin.core.model.MediaKind +import hu.bbara.purefin.core.model.Movie +import hu.bbara.purefin.core.model.Series +import java.util.UUID data class SearchResult( val id: UUID, val title: String, val posterUrl: String, - val type: BaseItemKind, + val type: MediaKind, ) { companion object { - fun create(movie: Movie, imageUrl: String) : SearchResult { + fun create(movie: Movie, imageUrl: String): SearchResult { return SearchResult( id = movie.id, title = movie.title, posterUrl = imageUrl, - type = BaseItemKind.MOVIE + type = MediaKind.MOVIE, ) } - fun create(series: Series, imageUrl: String) : SearchResult { + fun create(series: Series, imageUrl: String): SearchResult { return SearchResult( id = series.id, title = series.name, posterUrl = imageUrl, - type = BaseItemKind.SERIES + type = MediaKind.SERIES, ) } } diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/search/SearchViewModel.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/search/SearchViewModel.kt index d2f3ed85..f5c72085 100644 --- a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/search/SearchViewModel.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/search/SearchViewModel.kt @@ -3,10 +3,10 @@ package hu.bbara.purefin.feature.shared.search import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import hu.bbara.purefin.core.data.MediaRepository +import hu.bbara.purefin.core.data.MediaCatalogReader import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.data.session.UserSessionRepository -import hu.bbara.purefin.core.model.SearchResult +import java.util.UUID import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -16,14 +16,13 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import org.jellyfin.sdk.model.api.ImageType -import java.util.UUID import javax.inject.Inject @HiltViewModel @OptIn(FlowPreview::class) class SearchViewModel @Inject constructor( - private val mediaRepository: MediaRepository, - private val userSessionRepository: UserSessionRepository + private val mediaCatalogReader: MediaCatalogReader, + private val userSessionRepository: UserSessionRepository, ) : ViewModel() { private val _searchResult = MutableStateFlow>(emptyList()) @@ -34,8 +33,8 @@ class SearchViewModel @Inject constructor( init { combine( query.debounce(300).distinctUntilChanged(), - mediaRepository.movies, - mediaRepository.series + mediaCatalogReader.movies, + mediaCatalogReader.series ) { currentQuery, movies, series -> val filteredMovies = movies.filter { it.value.title.contains(currentQuery, ignoreCase = true) @@ -59,4 +58,4 @@ class SearchViewModel @Inject constructor( return JellyfinImageHelper.toImageUrl(userSessionRepository.serverUrl.first(), id, ImageType.PRIMARY) } -} \ No newline at end of file +} diff --git a/feature/shared/src/test/java/hu/bbara/purefin/feature/shared/navigation/RouteSerializationTest.kt b/feature/shared/src/test/java/hu/bbara/purefin/feature/shared/navigation/RouteSerializationTest.kt new file mode 100644 index 00000000..0f8028c7 --- /dev/null +++ b/feature/shared/src/test/java/hu/bbara/purefin/feature/shared/navigation/RouteSerializationTest.kt @@ -0,0 +1,66 @@ +package hu.bbara.purefin.feature.shared.navigation + +import java.util.UUID +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.junit.Assert.assertEquals +import org.junit.Test + +class RouteSerializationTest { + + private val json = Json + + @Test + fun `episode route round trips with nested dto ids`() { + val route: Route = Route.EpisodeRoute( + item = EpisodeDto( + id = UUID.randomUUID(), + seasonId = UUID.randomUUID(), + seriesId = UUID.randomUUID(), + ) + ) + + assertRoundTrip(route) + } + + @Test + fun `library route round trips`() { + val route: Route = Route.LibraryRoute( + library = LibraryDto( + id = UUID.randomUUID(), + name = "Shows", + ) + ) + + assertRoundTrip(route) + } + + @Test + fun `movie and series routes round trip`() { + assertRoundTrip( + Route.MovieRoute( + item = MovieDto(id = UUID.randomUUID()) + ) + ) + assertRoundTrip( + Route.SeriesRoute( + item = SeriesDto(id = UUID.randomUUID()) + ) + ) + } + + @Test + fun `singleton routes round trip`() { + assertRoundTrip(Route.Home) + assertRoundTrip(Route.LoginRoute) + assertRoundTrip(Route.PlayerRoute(mediaId = UUID.randomUUID().toString())) + } + + private fun assertRoundTrip(route: Route) { + val encoded = json.encodeToString(route) + val decoded = json.decodeFromString(encoded) + + assertEquals(route, decoded) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 51bccc50..a92b9da8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-viewmodel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } @@ -70,6 +71,7 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }