mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
refactor: update package structure and imports for consistency across UI components
This commit is contained in:
@@ -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.TvMediaDetailBodyBox
|
||||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||||
import hu.bbara.purefin.core.data.navigation.EpisodeDto
|
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
||||||
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
|
import hu.bbara.purefin.tv.navigation.LocalNavigationManager
|
||||||
import hu.bbara.purefin.core.data.navigation.Route
|
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
|||||||
@@ -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.TvMediaDetailBodyBox
|
||||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
||||||
import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl
|
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.core.model.Movie
|
||||||
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
||||||
|
|
||||||
|
|||||||
@@ -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.TvMediaDetailBodyBox
|
||||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
||||||
import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl
|
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.Season
|
||||||
import hu.bbara.purefin.core.model.Series
|
import hu.bbara.purefin.core.model.Series
|
||||||
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
|
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvSeriesScreen(
|
fun TvSeriesScreen(
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator
|
|||||||
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
||||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PosterCard(
|
fun PosterCard(
|
||||||
@@ -57,9 +57,9 @@ fun PosterCard(
|
|||||||
|
|
||||||
fun openItem(posterItem: PosterItem) {
|
fun openItem(posterItem: PosterItem) {
|
||||||
when (posterItem.type) {
|
when (posterItem.type) {
|
||||||
BaseItemKind.MOVIE -> onMovieSelected(posterItem.id)
|
MediaKind.MOVIE -> onMovieSelected(posterItem.id)
|
||||||
BaseItemKind.SERIES -> onSeriesSelected(posterItem.id)
|
MediaKind.SERIES -> onSeriesSelected(posterItem.id)
|
||||||
BaseItemKind.EPISODE -> {
|
MediaKind.EPISODE -> {
|
||||||
val ep = posterItem.episode!!
|
val ep = posterItem.episode!!
|
||||||
onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id)
|
onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id)
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ fun PosterCard(
|
|||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
when (item.type) {
|
when (item.type) {
|
||||||
BaseItemKind.MOVIE -> {
|
MediaKind.MOVIE -> {
|
||||||
val m = item.movie!!
|
val m = item.movie!!
|
||||||
WatchStateIndicator(
|
WatchStateIndicator(
|
||||||
size = indicatorSize,
|
size = indicatorSize,
|
||||||
@@ -109,7 +109,7 @@ fun PosterCard(
|
|||||||
started = (m.progress ?: 0.0) > 0
|
started = (m.progress ?: 0.0) > 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
BaseItemKind.EPISODE -> {
|
MediaKind.EPISODE -> {
|
||||||
val ep = item.episode!!
|
val ep = item.episode!!
|
||||||
WatchStateIndicator(
|
WatchStateIndicator(
|
||||||
size = indicatorSize,
|
size = indicatorSize,
|
||||||
@@ -119,7 +119,7 @@ fun PosterCard(
|
|||||||
started = (ep.progress ?: 0.0) > 0
|
started = (ep.progress ?: 0.0) > 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
BaseItemKind.SERIES -> UnwatchedEpisodeIndicator(
|
MediaKind.SERIES -> UnwatchedEpisodeIndicator(
|
||||||
size = indicatorSize,
|
size = indicatorSize,
|
||||||
modifier = Modifier.align(Alignment.TopEnd)
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
.padding(indicatorPadding),
|
.padding(indicatorPadding),
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
|
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.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.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.AppTheme
|
||||||
import hu.bbara.purefin.ui.theme.backgroundDark
|
import hu.bbara.purefin.ui.theme.backgroundDark
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|||||||
@@ -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.TvDrawerDestinationItem
|
||||||
import hu.bbara.purefin.tv.home.ui.TvNavigationDrawer
|
import hu.bbara.purefin.tv.home.ui.TvNavigationDrawer
|
||||||
import hu.bbara.purefin.tv.library.ui.TvLibrariesOverviewScreen
|
import hu.bbara.purefin.tv.library.ui.TvLibrariesOverviewScreen
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import hu.bbara.purefin.core.model.LibraryKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvAppScreen(
|
fun TvAppScreen(
|
||||||
@@ -48,8 +48,8 @@ fun TvAppScreen(
|
|||||||
destination = TvDrawerDestination.LIBRARIES,
|
destination = TvDrawerDestination.LIBRARIES,
|
||||||
label = "Libraries",
|
label = "Libraries",
|
||||||
icon = when {
|
icon = when {
|
||||||
libraries.any { it.type == CollectionType.MOVIES } -> Icons.Outlined.Movie
|
libraries.any { it.type == LibraryKind.MOVIES } -> Icons.Outlined.Movie
|
||||||
libraries.any { it.type == CollectionType.TVSHOWS } -> Icons.Outlined.Tv
|
libraries.any { it.type == LibraryKind.SERIES } -> Icons.Outlined.Tv
|
||||||
else -> Icons.Outlined.Collections
|
else -> Icons.Outlined.Collections
|
||||||
},
|
},
|
||||||
selected = selectedDestination == TvDrawerDestination.LIBRARIES
|
selected = selectedDestination == TvDrawerDestination.LIBRARIES
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import hu.bbara.purefin.core.data.download.MediaDownloadController
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|||||||
@@ -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.TvFocusedItemHero
|
||||||
import hu.bbara.purefin.tv.home.ui.TvHomeContent
|
import hu.bbara.purefin.tv.home.ui.TvHomeContent
|
||||||
import hu.bbara.purefin.tv.home.ui.rememberTvHomeHeroState
|
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 const val TvHomeHeroHeightFraction = 0.32f
|
||||||
private val TvHomeMinHeroHeight = 160.dp
|
private val TvHomeMinHeroHeight = 160.dp
|
||||||
|
|||||||
@@ -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.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -26,16 +26,16 @@ internal data class TvFocusedHeroModel(
|
|||||||
internal fun FocusableItem.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
internal fun FocusableItem.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is ContinueWatchingItem -> when (type) {
|
is ContinueWatchingItem -> when (type) {
|
||||||
BaseItemKind.MOVIE -> movie!!.toTvFocusedHeroModel()
|
MediaKind.MOVIE -> movie!!.toTvFocusedHeroModel()
|
||||||
BaseItemKind.EPISODE -> episode!!.toTvFocusedHeroModel()
|
MediaKind.EPISODE -> episode!!.toTvFocusedHeroModel()
|
||||||
else -> unsupportedType(type)
|
else -> unsupportedType(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NextUpItem -> episode.toTvFocusedHeroModel()
|
is NextUpItem -> episode.toTvFocusedHeroModel()
|
||||||
is PosterItem -> when (type) {
|
is PosterItem -> when (type) {
|
||||||
BaseItemKind.MOVIE -> movie!!.toTvFocusedHeroModel()
|
MediaKind.MOVIE -> movie!!.toTvFocusedHeroModel()
|
||||||
BaseItemKind.EPISODE -> episode!!.toTvFocusedHeroModel()
|
MediaKind.EPISODE -> episode!!.toTvFocusedHeroModel()
|
||||||
BaseItemKind.SERIES -> series!!.toTvFocusedHeroModel()
|
MediaKind.SERIES -> series!!.toTvFocusedHeroModel()
|
||||||
else -> unsupportedType(type)
|
else -> unsupportedType(type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,6 +117,6 @@ private fun seasonLabel(seasonCount: Int): String {
|
|||||||
return if (seasonCount == 1) "1 season" else "$seasonCount seasons"
|
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")
|
throw UnsupportedOperationException("Unsupported item type: $type")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
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 TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
||||||
internal const val TvHomeContentViewportTag = "tv-home-content-viewport"
|
internal const val TvHomeContentViewportTag = "tv-home-content-viewport"
|
||||||
|
|||||||
@@ -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.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
internal data class TvHomeItemRegistry(
|
internal data class TvHomeItemRegistry(
|
||||||
val visibleLibraries: List<LibraryItem>,
|
val visibleLibraries: List<LibraryItem>,
|
||||||
|
|||||||
@@ -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.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
|
||||||
private val TvHomeSectionsThumbShape = RoundedCornerShape(20.dp)
|
private val TvHomeSectionsThumbShape = RoundedCornerShape(20.dp)
|
||||||
@@ -93,12 +93,12 @@ fun TvContinueWatchingSection(
|
|||||||
title = item.primaryText,
|
title = item.primaryText,
|
||||||
supporting = item.secondaryText,
|
supporting = item.secondaryText,
|
||||||
imageUrl = when (item.type) {
|
imageUrl = when (item.type) {
|
||||||
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||||
prefixImageUrl = item.movie?.imageUrlPrefix,
|
prefixImageUrl = item.movie?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
imageType = ImageType.PRIMARY
|
||||||
)
|
)
|
||||||
|
|
||||||
BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
||||||
prefixImageUrl = item.episode?.imageUrlPrefix,
|
prefixImageUrl = item.episode?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
imageType = ImageType.PRIMARY
|
||||||
)
|
)
|
||||||
@@ -124,8 +124,8 @@ fun TvContinueWatchingSection(
|
|||||||
onFocusedItem = { onFocusedItem(item) },
|
onFocusedItem = { onFocusedItem(item) },
|
||||||
onClick = {
|
onClick = {
|
||||||
when (item.type) {
|
when (item.type) {
|
||||||
BaseItemKind.MOVIE -> onMovieSelected(item.movie!!.id)
|
MediaKind.MOVIE -> onMovieSelected(item.movie!!.id)
|
||||||
BaseItemKind.EPISODE -> {
|
MediaKind.EPISODE -> {
|
||||||
val episode = item.episode!!
|
val episode = item.episode!!
|
||||||
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
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-"
|
internal const val TvLibrariesOverviewItemTagPrefix = "tv-libraries-overview-item-"
|
||||||
|
|
||||||
@@ -161,9 +161,8 @@ private fun TvLibraryOverviewCard(
|
|||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = when (item.type) {
|
imageVector = when (item.type) {
|
||||||
CollectionType.MOVIES -> Icons.Outlined.Movie
|
LibraryKind.MOVIES -> Icons.Outlined.Movie
|
||||||
CollectionType.TVSHOWS -> Icons.Outlined.Tv
|
LibraryKind.SERIES -> Icons.Outlined.Tv
|
||||||
else -> Icons.Outlined.Collections
|
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = scheme.onSurfaceVariant,
|
tint = scheme.onSurfaceVariant,
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.PosterCard
|
import hu.bbara.purefin.common.ui.PosterCard
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinIconButton
|
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.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvLibraryScreen(
|
fun TvLibraryScreen(
|
||||||
|
|||||||
@@ -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<NavigationManager> =
|
||||||
|
staticCompositionLocalOf { error("NavigationManager not provided") }
|
||||||
|
|
||||||
|
val LocalNavigationBackStack = compositionLocalOf<List<Route>> { emptyList() }
|
||||||
@@ -6,7 +6,7 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.components.ActivityRetainedComponent
|
import dagger.hilt.android.components.ActivityRetainedComponent
|
||||||
import dagger.multibindings.IntoSet
|
import dagger.multibindings.IntoSet
|
||||||
import hu.bbara.purefin.core.data.navigation.Route
|
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(ActivityRetainedComponent::class)
|
@InstallIn(ActivityRetainedComponent::class)
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import androidx.navigation3.runtime.EntryProviderScope
|
|||||||
import hu.bbara.purefin.app.content.episode.TvEpisodeScreen
|
import hu.bbara.purefin.app.content.episode.TvEpisodeScreen
|
||||||
import hu.bbara.purefin.app.content.movie.TvMovieScreen
|
import hu.bbara.purefin.app.content.movie.TvMovieScreen
|
||||||
import hu.bbara.purefin.app.content.series.TvSeriesScreen
|
import hu.bbara.purefin.app.content.series.TvSeriesScreen
|
||||||
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
|
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||||
import hu.bbara.purefin.core.data.navigation.Route
|
|
||||||
import hu.bbara.purefin.login.ui.LoginScreen
|
import hu.bbara.purefin.login.ui.LoginScreen
|
||||||
import hu.bbara.purefin.tv.TvAppScreen
|
import hu.bbara.purefin.tv.TvAppScreen
|
||||||
import hu.bbara.purefin.tv.library.ui.TvLibraryScreen
|
import hu.bbara.purefin.tv.library.ui.TvLibraryScreen
|
||||||
|
|||||||
@@ -36,16 +36,16 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
|||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
import coil3.util.DebugLogger
|
import coil3.util.DebugLogger
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.JellyfinApiClient
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
|
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.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.AppTheme
|
||||||
import hu.bbara.purefin.ui.theme.backgroundDark
|
import hu.bbara.purefin.ui.theme.backgroundDark
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|||||||
@@ -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<Route>.homeSection() {
|
|
||||||
entry<Route.Home> {
|
|
||||||
AppScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<SearchResult>,
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.navigation
|
package hu.bbara.purefin.modules
|
||||||
|
|
||||||
import androidx.navigation3.runtime.EntryProviderScope
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@@ -6,7 +6,8 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.components.ActivityRetainedComponent
|
import dagger.hilt.android.components.ActivityRetainedComponent
|
||||||
import dagger.multibindings.IntoSet
|
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
|
@Module
|
||||||
@InstallIn(ActivityRetainedComponent::class)
|
@InstallIn(ActivityRetainedComponent::class)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.di
|
package hu.bbara.purefin.modules
|
||||||
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@@ -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<NavigationManager> =
|
||||||
|
staticCompositionLocalOf { error("NavigationManager not provided") }
|
||||||
|
|
||||||
|
val LocalNavigationBackStack = compositionLocalOf<List<Route>> { emptyList() }
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
package hu.bbara.purefin.navigation
|
package hu.bbara.purefin.navigation
|
||||||
|
|
||||||
import androidx.navigation3.runtime.EntryProviderScope
|
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.episode.EpisodeScreen
|
||||||
import hu.bbara.purefin.ui.screen.movie.MovieScreen
|
import hu.bbara.purefin.ui.screen.movie.MovieScreen
|
||||||
import hu.bbara.purefin.ui.screen.series.SeriesScreen
|
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<Route>.appRouteEntryBuilder() {
|
fun EntryProviderScope<Route>.appRouteEntryBuilder() {
|
||||||
entry<Route.Home> {
|
entry<Route.Home> {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel
|
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
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
|||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.layout.Box
|
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.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinSearchBar
|
|
||||||
import hu.bbara.purefin.feature.shared.search.SearchViewModel
|
import hu.bbara.purefin.feature.shared.search.SearchViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -56,24 +55,6 @@ fun DefaultTopBar(
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
contentAlignment = Alignment.CenterEnd
|
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) {
|
if (!isSearchExpanded) {
|
||||||
Box {
|
Box {
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -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.layout.padding
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -77,7 +77,7 @@ fun MediaResumeButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ButtonContent(text: String, color: androidx.compose.ui.graphics.Color) {
|
private fun ButtonContent(text: String, color: Color) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -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.foundation.layout.RowScope
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.border
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil3.request.ImageRequest
|
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 hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
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
|
@Composable
|
||||||
fun PosterCard(
|
fun PosterCard(
|
||||||
@@ -47,9 +47,9 @@ fun PosterCard(
|
|||||||
|
|
||||||
fun openItem(posterItem: PosterItem) {
|
fun openItem(posterItem: PosterItem) {
|
||||||
when (posterItem.type) {
|
when (posterItem.type) {
|
||||||
BaseItemKind.MOVIE -> onMovieSelected(posterItem.id)
|
MediaKind.MOVIE -> onMovieSelected(posterItem.id)
|
||||||
BaseItemKind.SERIES -> onSeriesSelected(posterItem.id)
|
MediaKind.SERIES -> onSeriesSelected(posterItem.id)
|
||||||
BaseItemKind.EPISODE -> {
|
MediaKind.EPISODE -> {
|
||||||
val ep = posterItem.episode!!
|
val ep = posterItem.episode!!
|
||||||
onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id)
|
onEpisodeSelected(ep.seriesId, ep.seasonId, ep.id)
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ fun PosterCard(
|
|||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
when (item.type) {
|
when (item.type) {
|
||||||
BaseItemKind.MOVIE -> {
|
MediaKind.MOVIE -> {
|
||||||
val m = item.movie!!
|
val m = item.movie!!
|
||||||
WatchStateIndicator(
|
WatchStateIndicator(
|
||||||
size = 28,
|
size = 28,
|
||||||
@@ -88,7 +88,7 @@ fun PosterCard(
|
|||||||
started = (m.progress ?: 0.0) > 0
|
started = (m.progress ?: 0.0) > 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
BaseItemKind.EPISODE -> {
|
MediaKind.EPISODE -> {
|
||||||
val ep = item.episode!!
|
val ep = item.episode!!
|
||||||
WatchStateIndicator(
|
WatchStateIndicator(
|
||||||
size = 28,
|
size = 28,
|
||||||
@@ -98,7 +98,7 @@ fun PosterCard(
|
|||||||
started = (ep.progress ?: 0.0) > 0
|
started = (ep.progress ?: 0.0) > 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
BaseItemKind.SERIES -> UnwatchedEpisodeIndicator(
|
MediaKind.SERIES -> UnwatchedEpisodeIndicator(
|
||||||
size = 28,
|
size = 28,
|
||||||
modifier = Modifier.align(Alignment.TopEnd)
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
@@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -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.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.common.ui.components
|
package hu.bbara.purefin.ui.common.image
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.border
|
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.TextUnit
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.core.model.CastMember
|
||||||
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaMetaChip(
|
fun MediaMetaChip(
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.layout.Box
|
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.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaHero(
|
fun MediaHero(
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -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.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -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.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -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.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -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.AnimatedVisibility
|
||||||
import androidx.compose.animation.EnterTransition
|
import androidx.compose.animation.EnterTransition
|
||||||
@@ -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.Icons
|
||||||
import androidx.compose.material.icons.outlined.Collections
|
import androidx.compose.material.icons.outlined.Collections
|
||||||
@@ -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.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -10,8 +10,11 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
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.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
|
@Composable
|
||||||
fun AppScreen(
|
fun AppScreen(
|
||||||
@@ -52,7 +55,12 @@ fun AppScreen(
|
|||||||
onMovieSelected = viewModel::onMovieSelected,
|
onMovieSelected = viewModel::onMovieSelected,
|
||||||
onSeriesSelected = viewModel::onSeriesSelected,
|
onSeriesSelected = viewModel::onSeriesSelected,
|
||||||
onEpisodeSelected = viewModel::onEpisodeSelected,
|
onEpisodeSelected = viewModel::onEpisodeSelected,
|
||||||
onLibrarySelected = { library -> viewModel.onLibrarySelected(library.id, library.name) },
|
onLibrarySelected = { library ->
|
||||||
|
viewModel.onLibrarySelected(
|
||||||
|
library.id,
|
||||||
|
library.name
|
||||||
|
)
|
||||||
|
},
|
||||||
onProfileClick = {},
|
onProfileClick = {},
|
||||||
onSettingsClick = {},
|
onSettingsClick = {},
|
||||||
onLogoutClick = viewModel::logout,
|
onLogoutClick = viewModel::logout,
|
||||||
@@ -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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -6,7 +6,8 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
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
|
@Composable
|
||||||
fun DownloadsScreen(
|
fun DownloadsScreen(
|
||||||
@@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
import hu.bbara.purefin.feature.shared.download.ActiveDownloadItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
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
|
import hu.bbara.purefin.feature.shared.download.DownloadsViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -34,14 +34,14 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
import hu.bbara.purefin.ui.common.media.MediaMetaChip
|
||||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.common.ui.components.MediaHero
|
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.data.download.DownloadState
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||||
import hu.bbara.purefin.core.data.navigation.EpisodeDto
|
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
||||||
import hu.bbara.purefin.core.data.navigation.LocalNavigationBackStack
|
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
||||||
import hu.bbara.purefin.core.data.navigation.Route
|
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||||
import hu.bbara.purefin.core.model.CastMember
|
import hu.bbara.purefin.core.model.CastMember
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.common.ui.MediaCastRow
|
import hu.bbara.purefin.ui.common.media.MediaCastRow
|
||||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
||||||
import hu.bbara.purefin.common.ui.components.GhostIconButton
|
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
import hu.bbara.purefin.ui.common.button.MediaActionButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
|
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
||||||
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.data.download.DownloadState
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
|
|||||||
@@ -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.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import hu.bbara.purefin.app.home.ui.HomeContent
|
import hu.bbara.purefin.ui.screen.home.components.HomeContent
|
||||||
import hu.bbara.purefin.app.home.ui.HomeTopBar
|
import hu.bbara.purefin.ui.screen.home.components.HomeTopBar
|
||||||
import hu.bbara.purefin.app.home.ui.search.HomeSearchOverlay
|
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.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.app.home.ui
|
package hu.bbara.purefin.ui.screen.home.components
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -22,18 +22,17 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import hu.bbara.purefin.ui.screen.home.components.continuewatching.ContinueWatchingSection
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -106,9 +105,9 @@ fun HomeContent(
|
|||||||
items = suggestions,
|
items = suggestions,
|
||||||
onItemOpen = { item ->
|
onItemOpen = { item ->
|
||||||
when (item.type) {
|
when (item.type) {
|
||||||
BaseItemKind.MOVIE -> onMovieSelected(item.id)
|
MediaKind.MOVIE -> onMovieSelected(item.id)
|
||||||
BaseItemKind.SERIES -> onSeriesSelected(item.id)
|
MediaKind.SERIES -> onSeriesSelected(item.id)
|
||||||
BaseItemKind.EPISODE -> onEpisodeSelected(item.id, item.id, item.id)
|
MediaKind.EPISODE -> onEpisodeSelected(item.id, item.id, item.id)
|
||||||
else -> {
|
else -> {
|
||||||
Log.e("HomeContent", "Unsupported item type: ${item.type}")
|
Log.e("HomeContent", "Unsupported item type: ${item.type}")
|
||||||
}
|
}
|
||||||
@@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import hu.bbara.purefin.ui.common.badge.ContentBadge
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeEmptyState(
|
fun HomeEmptyState(
|
||||||
@@ -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(
|
data class HomeNavItem(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
@@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.clickable
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
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.core.data.image.JellyfinImageHelper
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -40,12 +40,12 @@ internal fun ContinueWatchingCard(
|
|||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
val supportingText = when (item.type) {
|
val supportingText = when (item.type) {
|
||||||
BaseItemKind.MOVIE -> listOf(
|
MediaKind.MOVIE -> listOf(
|
||||||
item.movie?.year,
|
item.movie?.year,
|
||||||
item.movie?.runtime
|
item.movie?.runtime
|
||||||
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
||||||
|
|
||||||
BaseItemKind.EPISODE -> listOf(
|
MediaKind.EPISODE -> listOf(
|
||||||
"Episode ${item.episode?.index}",
|
"Episode ${item.episode?.index}",
|
||||||
item.episode?.runtime
|
item.episode?.runtime
|
||||||
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
||||||
@@ -53,11 +53,11 @@ internal fun ContinueWatchingCard(
|
|||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
val imageUrl = when (item.type) {
|
val imageUrl = when (item.type) {
|
||||||
BaseItemKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||||
prefixImageUrl = item.movie?.imageUrlPrefix,
|
prefixImageUrl = item.movie?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
imageType = ImageType.PRIMARY
|
||||||
)
|
)
|
||||||
BaseItemKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
||||||
prefixImageUrl = item.episode?.imageUrlPrefix,
|
prefixImageUrl = item.episode?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
imageType = ImageType.PRIMARY
|
||||||
)
|
)
|
||||||
@@ -74,8 +74,8 @@ internal fun ContinueWatchingCard(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
when (item.type) {
|
when (item.type) {
|
||||||
BaseItemKind.MOVIE -> onMovieSelected(item.movie!!.id)
|
MediaKind.MOVIE -> onMovieSelected(item.movie!!.id)
|
||||||
BaseItemKind.EPISODE -> {
|
MediaKind.EPISODE -> {
|
||||||
val episode = item.episode!!
|
val episode = item.episode!!
|
||||||
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
||||||
}
|
}
|
||||||
@@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -9,9 +9,9 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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 hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContinueWatchingSection(
|
fun ContinueWatchingSection(
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.clickable
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -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.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.clickable
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.common.ui.components.UnwatchedEpisodeIndicator
|
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator
|
||||||
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun HomeBrowseCard(
|
internal fun HomeBrowseCard(
|
||||||
@@ -40,16 +40,16 @@ internal fun HomeBrowseCard(
|
|||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
val supportingText = when (item.type) {
|
val supportingText = when (item.type) {
|
||||||
BaseItemKind.MOVIE -> listOf(
|
MediaKind.MOVIE -> listOf(
|
||||||
item.movie?.year,
|
item.movie?.year,
|
||||||
item.movie?.runtime
|
item.movie?.runtime
|
||||||
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
).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"
|
if (series.seasonCount == 1) "1 season" else "${series.seasonCount} seasons"
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseItemKind.EPISODE -> listOf(
|
MediaKind.EPISODE -> listOf(
|
||||||
"Episode ${item.episode?.index}",
|
"Episode ${item.episode?.index}",
|
||||||
item.episode?.runtime
|
item.episode?.runtime
|
||||||
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
).filterNotNull().filter { it.isNotBlank() }.joinToString(" • ")
|
||||||
@@ -67,9 +67,9 @@ internal fun HomeBrowseCard(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
when (item.type) {
|
when (item.type) {
|
||||||
BaseItemKind.MOVIE -> onMovieSelected(item.id)
|
MediaKind.MOVIE -> onMovieSelected(item.id)
|
||||||
BaseItemKind.SERIES -> onSeriesSelected(item.id)
|
MediaKind.SERIES -> onSeriesSelected(item.id)
|
||||||
BaseItemKind.EPISODE -> {
|
MediaKind.EPISODE -> {
|
||||||
val episode = item.episode!!
|
val episode = item.episode!!
|
||||||
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ internal fun HomeBrowseCard(
|
|||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
when (item.type) {
|
when (item.type) {
|
||||||
BaseItemKind.MOVIE -> {
|
MediaKind.MOVIE -> {
|
||||||
val movie = item.movie!!
|
val movie = item.movie!!
|
||||||
WatchStateIndicator(
|
WatchStateIndicator(
|
||||||
size = 28,
|
size = 28,
|
||||||
@@ -104,7 +104,7 @@ internal fun HomeBrowseCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseItemKind.EPISODE -> {
|
MediaKind.EPISODE -> {
|
||||||
val episode = item.episode!!
|
val episode = item.episode!!
|
||||||
WatchStateIndicator(
|
WatchStateIndicator(
|
||||||
size = 28,
|
size = 28,
|
||||||
@@ -116,7 +116,7 @@ internal fun HomeBrowseCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseItemKind.SERIES -> {
|
MediaKind.SERIES -> {
|
||||||
UnwatchedEpisodeIndicator(
|
UnwatchedEpisodeIndicator(
|
||||||
size = 28,
|
size = 28,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -9,10 +9,10 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
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
|
@Composable
|
||||||
fun LibraryPosterSection(
|
fun LibraryPosterSection(
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.clickable
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.core.data.image.JellyfinImageHelper
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
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
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -9,9 +9,9 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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 hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NextUpSection(
|
fun NextUpSection(
|
||||||
@@ -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.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -36,8 +36,8 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.feature.shared.search.SearchViewModel
|
import hu.bbara.purefin.feature.shared.search.SearchViewModel
|
||||||
import org.jellyfin.sdk.model.UUID
|
import java.util.UUID
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -142,8 +142,8 @@ fun HomeSearchOverlay(
|
|||||||
item = item,
|
item = item,
|
||||||
onClick = {
|
onClick = {
|
||||||
when (item.type) {
|
when (item.type) {
|
||||||
BaseItemKind.MOVIE -> onMovieSelected(item.id)
|
MediaKind.MOVIE -> onMovieSelected(item.id)
|
||||||
BaseItemKind.SERIES -> onSeriesSelected(item.id)
|
MediaKind.SERIES -> onSeriesSelected(item.id)
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import hu.bbara.purefin.core.model.SearchResult
|
import hu.bbara.purefin.feature.shared.search.SearchResult
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun HomeSearchResultCard(
|
internal fun HomeSearchResultCard(
|
||||||
@@ -59,8 +59,8 @@ internal fun HomeSearchResultCard(
|
|||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = when (item.type) {
|
text = when (item.type) {
|
||||||
BaseItemKind.MOVIE -> "Movie"
|
MediaKind.MOVIE -> "Movie"
|
||||||
BaseItemKind.SERIES -> "Series"
|
MediaKind.SERIES -> "Series"
|
||||||
else -> "Title"
|
else -> "Title"
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
@@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -6,9 +6,10 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import hu.bbara.purefin.app.home.ui.DefaultTopBar
|
import hu.bbara.purefin.ui.common.DefaultTopBar
|
||||||
import hu.bbara.purefin.app.home.ui.HomeNavItem
|
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
||||||
import hu.bbara.purefin.app.home.ui.libraries.LibrariesContent
|
import hu.bbara.purefin.ui.screen.AppBottomBar
|
||||||
|
import hu.bbara.purefin.ui.screen.libraries.components.LibrariesContent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibrariesScreen(
|
fun LibrariesScreen(
|
||||||
@@ -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.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.app.home.ui.HomeNavItem
|
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibrariesContent(
|
fun LibrariesContent(
|
||||||
@@ -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.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.app.home.ui.HomeNavItem
|
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryListItem(
|
fun LibraryListItem(
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -21,9 +21,9 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
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.common.ui.components.PurefinIconButton
|
import hu.bbara.purefin.ui.common.button.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.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
||||||
|
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.PurefinComplexTextField
|
import hu.bbara.purefin.ui.common.textfield.PurefinComplexTextField
|
||||||
import hu.bbara.purefin.common.ui.PurefinPasswordField
|
import hu.bbara.purefin.ui.common.textfield.PurefinPasswordField
|
||||||
import hu.bbara.purefin.common.ui.PurefinTextButton
|
import hu.bbara.purefin.ui.common.button.PurefinTextButton
|
||||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.feature.shared.login.LoginViewModel
|
import hu.bbara.purefin.feature.shared.login.LoginViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -32,12 +32,12 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
import hu.bbara.purefin.ui.common.media.MediaMetaChip
|
||||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.common.ui.components.MediaHero
|
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.data.download.DownloadState
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
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.CastMember
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.common.ui.MediaCastRow
|
import hu.bbara.purefin.ui.common.media.MediaCastRow
|
||||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
||||||
import hu.bbara.purefin.common.ui.components.GhostIconButton
|
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
import hu.bbara.purefin.ui.common.button.MediaActionButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
|
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
||||||
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.data.download.DownloadState
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.player.ui
|
package hu.bbara.purefin.ui.screen.player
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -37,16 +37,16 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.ui.AspectRatioFrameLayout
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
import androidx.media3.ui.PlayerView
|
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.core.player.viewmodel.PlayerViewModel
|
||||||
import hu.bbara.purefin.player.ui.components.PersistentOverlayContainer
|
import hu.bbara.purefin.ui.common.visual.EmptyValueTimedVisibility
|
||||||
import hu.bbara.purefin.player.ui.components.PlayerAdjustmentIndicator
|
import hu.bbara.purefin.ui.common.visual.ValueChangeTimedVisibility
|
||||||
import hu.bbara.purefin.player.ui.components.PlayerControlsOverlay
|
import hu.bbara.purefin.ui.screen.player.components.PersistentOverlayContainer
|
||||||
import hu.bbara.purefin.player.ui.components.PlayerGesturesLayer
|
import hu.bbara.purefin.ui.screen.player.components.PlayerAdjustmentIndicator
|
||||||
import hu.bbara.purefin.player.ui.components.PlayerLoadingErrorEndCard
|
import hu.bbara.purefin.ui.screen.player.components.PlayerControlsOverlay
|
||||||
import hu.bbara.purefin.player.ui.components.PlayerQueuePanel
|
import hu.bbara.purefin.ui.screen.player.components.PlayerGesturesLayer
|
||||||
import hu.bbara.purefin.player.ui.components.rememberPersistentOverlayController
|
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.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ fun PlayerScreen(
|
|||||||
onTap = { viewModel.toggleControlsVisibility() },
|
onTap = { viewModel.toggleControlsVisibility() },
|
||||||
onDoubleTapRight = { viewModel.seekBy(30_000) },
|
onDoubleTapRight = { viewModel.seekBy(30_000) },
|
||||||
onDoubleTapLeft = { viewModel.seekBy(-10_000) },
|
onDoubleTapLeft = { viewModel.seekBy(-10_000) },
|
||||||
onDoubleTapCenter = {viewModel.togglePlayPause()},
|
onDoubleTapCenter = { viewModel.togglePlayPause() },
|
||||||
onVerticalDragLeft = { delta ->
|
onVerticalDragLeft = { delta ->
|
||||||
val diff = (-delta / 800f)
|
val diff = (-delta / 800f)
|
||||||
brightness = (brightness + diff).coerceIn(0f, 1f)
|
brightness = (brightness + diff).coerceIn(0f, 1f)
|
||||||
@@ -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.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.clickable
|
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.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.common.ui.components.GhostIconButton
|
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinIconButton
|
import hu.bbara.purefin.ui.common.button.PurefinIconButton
|
||||||
import hu.bbara.purefin.core.player.model.PlayerUiState
|
import hu.bbara.purefin.core.player.model.PlayerUiState
|
||||||
import hu.bbara.purefin.core.player.model.TrackOption
|
import hu.bbara.purefin.core.player.model.TrackOption
|
||||||
|
|
||||||
@@ -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.awaitEachGesture
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
@@ -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.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -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.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
import hu.bbara.purefin.core.player.model.PlayerUiState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -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.Canvas
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -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.background
|
||||||
import androidx.compose.foundation.clickable
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
import hu.bbara.purefin.core.player.model.TrackOption
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -30,12 +30,12 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
||||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.common.ui.components.MediaHero
|
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.data.download.DownloadState
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
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.CastMember
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.core.model.Season
|
import hu.bbara.purefin.core.model.Season
|
||||||
|
|||||||
@@ -57,19 +57,19 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.common.ui.MediaCastRow
|
import hu.bbara.purefin.ui.common.media.MediaCastRow
|
||||||
import hu.bbara.purefin.common.ui.MediaMetaChip
|
import hu.bbara.purefin.ui.common.media.MediaMetaChip
|
||||||
import hu.bbara.purefin.common.ui.components.GhostIconButton
|
import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaActionButton
|
import hu.bbara.purefin.ui.common.button.MediaActionButton
|
||||||
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.data.download.DownloadState
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||||
import hu.bbara.purefin.core.data.navigation.EpisodeDto
|
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
||||||
import hu.bbara.purefin.core.data.navigation.LocalNavigationManager
|
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||||
import hu.bbara.purefin.core.data.navigation.Route
|
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||||
import hu.bbara.purefin.core.model.CastMember
|
import hu.bbara.purefin.core.model.CastMember
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.core.model.Season
|
import hu.bbara.purefin.core.model.Season
|
||||||
|
|||||||
@@ -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.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.RepeatMode
|
import androidx.compose.animation.core.RepeatMode
|
||||||
@@ -3,6 +3,7 @@ plugins {
|
|||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.android.library) apply false
|
alias(libs.plugins.android.library) apply false
|
||||||
alias(libs.plugins.kotlin.android) 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.compose) apply false
|
||||||
alias(libs.plugins.kotlin.serialization) apply false
|
alias(libs.plugins.kotlin.serialization) apply false
|
||||||
alias(libs.plugins.ksp) apply false
|
alias(libs.plugins.ksp) apply false
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.library)
|
alias(libs.plugins.android.library)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
|
||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
alias(libs.plugins.hilt)
|
alias(libs.plugins.hilt)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
@@ -21,10 +20,6 @@ android {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
compose = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@@ -43,12 +38,7 @@ dependencies {
|
|||||||
implementation(libs.datastore)
|
implementation(libs.datastore)
|
||||||
implementation(libs.okhttp)
|
implementation(libs.okhttp)
|
||||||
implementation(libs.logging.interceptor)
|
implementation(libs.logging.interceptor)
|
||||||
implementation(libs.coil.compose)
|
|
||||||
implementation(libs.coil.network.okhttp)
|
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
implementation(libs.androidx.navigation3.runtime)
|
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
|
||||||
implementation(libs.androidx.compose.ui)
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package hu.bbara.purefin.core.data
|
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.Episode
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.core.model.Series
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -12,41 +13,22 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.stateIn
|
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)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Singleton
|
@Singleton
|
||||||
class CompositeMediaRepository @Inject constructor(
|
class CompositeMediaRepository @Inject constructor(
|
||||||
private val offlineRepository: OfflineMediaRepository,
|
private val offlineRepository: OfflineMediaRepository,
|
||||||
private val onlineRepository: InMemoryMediaRepository,
|
private val onlineRepository: InMemoryMediaRepository,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
) : MediaRepository {
|
) : MediaCatalogReader, MediaProgressWriter {
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
override val ready: StateFlow<Boolean> = combine(
|
private val activeRepository: Flow<MediaCatalogReader> =
|
||||||
offlineRepository.ready,
|
|
||||||
onlineRepository.ready
|
|
||||||
) { offlineReady, onlineReady ->
|
|
||||||
offlineReady && onlineReady
|
|
||||||
}.stateIn(scope, SharingStarted.Eagerly, false)
|
|
||||||
|
|
||||||
private val isOnline: StateFlow<Boolean> = networkMonitor.isOnline
|
|
||||||
.stateIn(scope, SharingStarted.Eagerly, false)
|
|
||||||
|
|
||||||
private val activeRepository: Flow<MediaRepository> =
|
|
||||||
networkMonitor.isOnline.flatMapLatest { online ->
|
networkMonitor.isOnline.flatMapLatest { online ->
|
||||||
flowOf(if (online) onlineRepository else offlineRepository)
|
flowOf(if (online) onlineRepository else offlineRepository)
|
||||||
}
|
}
|
||||||
@@ -63,42 +45,12 @@ class CompositeMediaRepository @Inject constructor(
|
|||||||
.flatMapLatest { it.episodes }
|
.flatMapLatest { it.episodes }
|
||||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||||
|
|
||||||
override fun upsertMovies(movies: List<Movie>) {
|
|
||||||
if (!isOnline.value) {
|
|
||||||
Log.e("CompositeMediaRepository", "upsertMovies called in offline mode")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onlineRepository.upsertMovies(movies)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun upsertSeries(series: List<Series>) {
|
|
||||||
if (!isOnline.value) {
|
|
||||||
Log.e("CompositeMediaRepository", "upsertSeries called in offline mode")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onlineRepository.upsertSeries(series)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun upsertEpisodes(episodes: List<Episode>) {
|
|
||||||
if (!isOnline.value) {
|
|
||||||
Log.e("CompositeMediaRepository", "upsertEpisodes called in offline mode")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onlineRepository.upsertEpisodes(episodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
||||||
return activeRepository.flatMapLatest { it.observeSeriesWithContent(seriesId) }
|
return activeRepository.flatMapLatest { it.observeSeriesWithContent(seriesId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
||||||
val isOnline = networkMonitor.isOnline.stateIn(scope).value
|
val repository = if (networkMonitor.isOnline.first()) onlineRepository else offlineRepository
|
||||||
val repo = if (isOnline) onlineRepository else offlineRepository
|
repository.updateWatchProgress(mediaId, positionMs, durationMs)
|
||||||
repo.updateWatchProgress(mediaId, positionMs, durationMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setReady() {
|
|
||||||
onlineRepository.setReady()
|
|
||||||
offlineRepository.setReady()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.core.player.data
|
package hu.bbara.purefin.core.data
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
@@ -6,29 +6,29 @@ import androidx.core.net.toUri
|
|||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.util.UnstableApi
|
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.JellyfinApiClient
|
||||||
import hu.bbara.purefin.core.data.client.PlaybackDecision
|
import hu.bbara.purefin.core.data.client.PlaybackDecision
|
||||||
import hu.bbara.purefin.core.data.client.PlaybackReportContext
|
import hu.bbara.purefin.core.data.client.PlaybackReportContext
|
||||||
import hu.bbara.purefin.core.data.client.playbackCustomCacheKey
|
import hu.bbara.purefin.core.data.client.playbackCustomCacheKey
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ViewModelScoped
|
@Singleton
|
||||||
class PlayerMediaRepository @Inject constructor(
|
class DefaultPlayableMediaRepository @Inject constructor(
|
||||||
private val jellyfinApiClient: JellyfinApiClient,
|
private val jellyfinApiClient: JellyfinApiClient,
|
||||||
private val userSessionRepository: UserSessionRepository
|
private val userSessionRepository: UserSessionRepository,
|
||||||
) {
|
) : PlayableMediaRepository {
|
||||||
|
|
||||||
suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? = withContext(Dispatchers.IO) {
|
override suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? = withContext(Dispatchers.IO) {
|
||||||
val playbackDecision = jellyfinApiClient.getPlaybackDecision(mediaId) ?: return@withContext null
|
val playbackDecision = jellyfinApiClient.getPlaybackDecision(mediaId) ?: return@withContext null
|
||||||
val baseItem = jellyfinApiClient.getItemInfo(mediaId)
|
val baseItem = jellyfinApiClient.getItemInfo(mediaId)
|
||||||
|
|
||||||
@@ -49,7 +49,11 @@ class PlayerMediaRepository @Inject constructor(
|
|||||||
Pair(mediaItem, resumePositionMs)
|
Pair(mediaItem, resumePositionMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getNextUpMediaItems(episodeId: UUID, existingIds: Set<String>, count: Int = 9): List<MediaItem> = withContext(Dispatchers.IO) {
|
override suspend fun getNextUpMediaItems(
|
||||||
|
episodeId: UUID,
|
||||||
|
existingIds: Set<String>,
|
||||||
|
count: Int,
|
||||||
|
): List<MediaItem> = withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val serverUrl = userSessionRepository.serverUrl.first()
|
val serverUrl = userSessionRepository.serverUrl.first()
|
||||||
val episodes = jellyfinApiClient.getNextEpisodes(episodeId = episodeId, count = count)
|
val episodes = jellyfinApiClient.getNextEpisodes(episodeId = episodeId, count = count)
|
||||||
@@ -71,7 +75,7 @@ class PlayerMediaRepository @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.getOrElse { error ->
|
}.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()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +103,7 @@ class PlayerMediaRepository @Inject constructor(
|
|||||||
playbackCustomCacheKey(
|
playbackCustomCacheKey(
|
||||||
mediaId = mediaId,
|
mediaId = mediaId,
|
||||||
playbackUrl = playbackDecision.url,
|
playbackUrl = playbackDecision.url,
|
||||||
playMethod = playbackDecision.reportContext.playMethod
|
playMethod = playbackDecision.reportContext.playMethod,
|
||||||
)?.let(builder::setCustomCacheKey)
|
)?.let(builder::setCustomCacheKey)
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
@@ -107,25 +111,17 @@ class PlayerMediaRepository @Inject constructor(
|
|||||||
|
|
||||||
private fun calculateResumePosition(
|
private fun calculateResumePosition(
|
||||||
baseItem: BaseItemDto?,
|
baseItem: BaseItemDto?,
|
||||||
mediaSource: MediaSourceInfo
|
mediaSource: MediaSourceInfo,
|
||||||
): Long? {
|
): Long? {
|
||||||
val userData = baseItem?.userData ?: return null
|
val userData = baseItem?.userData ?: return null
|
||||||
|
|
||||||
// Get runtime in ticks
|
|
||||||
val runtimeTicks = mediaSource.runTimeTicks ?: baseItem.runTimeTicks ?: 0L
|
val runtimeTicks = mediaSource.runTimeTicks ?: baseItem.runTimeTicks ?: 0L
|
||||||
if (runtimeTicks == 0L) return null
|
if (runtimeTicks == 0L) return null
|
||||||
|
|
||||||
// Get saved playback position from userData
|
|
||||||
val playbackPositionTicks = userData.playbackPositionTicks ?: 0L
|
val playbackPositionTicks = userData.playbackPositionTicks ?: 0L
|
||||||
if (playbackPositionTicks == 0L) return null
|
if (playbackPositionTicks == 0L) return null
|
||||||
|
|
||||||
// Convert ticks to milliseconds
|
|
||||||
val positionMs = playbackPositionTicks / 10_000
|
val positionMs = playbackPositionTicks / 10_000
|
||||||
|
|
||||||
// Calculate percentage for threshold check
|
|
||||||
val percentage = (playbackPositionTicks.toDouble() / runtimeTicks.toDouble()) * 100.0
|
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
|
return if (percentage in 5.0..95.0) positionMs else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package hu.bbara.purefin.core.data
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface EpisodeSeriesLookup {
|
||||||
|
suspend fun preferenceKeyFor(mediaId: UUID): String
|
||||||
|
}
|
||||||
@@ -2,11 +2,10 @@ package hu.bbara.purefin.core.data
|
|||||||
|
|
||||||
import hu.bbara.purefin.core.model.Library
|
import hu.bbara.purefin.core.model.Library
|
||||||
import hu.bbara.purefin.core.model.Media
|
import hu.bbara.purefin.core.model.Media
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface AppContentRepository : MediaRepository {
|
interface HomeRepository {
|
||||||
|
|
||||||
val libraries: StateFlow<List<Library>>
|
val libraries: StateFlow<List<Library>>
|
||||||
val suggestions: StateFlow<List<Media>>
|
val suggestions: StateFlow<List<Media>>
|
||||||
val continueWatching: StateFlow<List<Media>>
|
val continueWatching: StateFlow<List<Media>>
|
||||||
@@ -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.data.session.UserSessionRepository
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.core.model.Library
|
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.Media
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.core.model.Season
|
import hu.bbara.purefin.core.model.Season
|
||||||
import hu.bbara.purefin.core.model.Series
|
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.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -37,75 +21,114 @@ import java.util.UUID
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
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
|
@Singleton
|
||||||
class InMemoryAppContentRepository @Inject constructor(
|
class InMemoryAppContentRepository @Inject constructor(
|
||||||
val userSessionRepository: UserSessionRepository,
|
val userSessionRepository: UserSessionRepository,
|
||||||
val jellyfinApiClient: JellyfinApiClient,
|
val jellyfinApiClient: JellyfinApiClient,
|
||||||
private val homeCacheDataStore: DataStore<HomeCache>,
|
private val homeCacheDataStore: DataStore<HomeCache>,
|
||||||
private val mediaRepository: CompositeMediaRepository,
|
private val onlineMediaRepository: InMemoryMediaRepository,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
) : AppContentRepository {
|
) : HomeRepository {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
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<Boolean> = MutableStateFlow(false)
|
private val librariesState = MutableStateFlow<List<Library>>(emptyList())
|
||||||
override val ready: StateFlow<Boolean> = combine(contentRepositoryReady, mediaRepository.ready) {
|
override val libraries: StateFlow<List<Library>> = librariesState.asStateFlow()
|
||||||
contentRepositoryReady, mediaRepositoryReady ->
|
|
||||||
contentRepositoryReady && mediaRepositoryReady
|
|
||||||
}.stateIn(scope, SharingStarted.Eagerly, false)
|
|
||||||
|
|
||||||
private val _libraries: MutableStateFlow<List<Library>> = MutableStateFlow(emptyList())
|
private val suggestionsState = MutableStateFlow<List<Media>>(emptyList())
|
||||||
|
override val suggestions: StateFlow<List<Media>> = suggestionsState.asStateFlow()
|
||||||
|
|
||||||
override val libraries: StateFlow<List<Library>> = _libraries.asStateFlow()
|
private val continueWatchingState = MutableStateFlow<List<Media>>(emptyList())
|
||||||
private val _suggestions: MutableStateFlow<List<Media>> = MutableStateFlow(emptyList())
|
override val continueWatching: StateFlow<List<Media>> = continueWatchingState.asStateFlow()
|
||||||
|
|
||||||
override val suggestions: StateFlow<List<Media>> = _suggestions.asStateFlow()
|
private val nextUpState = MutableStateFlow<List<Media>>(emptyList())
|
||||||
private val _continueWatching: MutableStateFlow<List<Media>> = MutableStateFlow(emptyList())
|
override val nextUp: StateFlow<List<Media>> = nextUpState.asStateFlow()
|
||||||
|
|
||||||
override val continueWatching: StateFlow<List<Media>> = _continueWatching.asStateFlow()
|
private val latestLibraryContentState = MutableStateFlow<Map<UUID, List<Media>>>(emptyMap())
|
||||||
private val _nextUp: MutableStateFlow<List<Media>> = MutableStateFlow(emptyList())
|
override val latestLibraryContent: StateFlow<Map<UUID, List<Media>>> = latestLibraryContentState.asStateFlow()
|
||||||
|
|
||||||
override val nextUp: StateFlow<List<Media>> = _nextUp.asStateFlow()
|
|
||||||
private val _latestLibraryContent: MutableStateFlow<Map<UUID, List<Media>>> = MutableStateFlow(emptyMap())
|
|
||||||
override val latestLibraryContent: StateFlow<Map<UUID, List<Media>>> = _latestLibraryContent.asStateFlow()
|
|
||||||
|
|
||||||
override val movies: StateFlow<Map<UUID, Movie>> = mediaRepository.movies
|
|
||||||
override val series: StateFlow<Map<UUID, Series>> = mediaRepository.series
|
|
||||||
override val episodes: StateFlow<Map<UUID, Episode>> = mediaRepository.episodes
|
|
||||||
|
|
||||||
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
|
||||||
return mediaRepository.observeSeriesWithContent(seriesId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateWatchProgress(
|
|
||||||
mediaId: UUID,
|
|
||||||
positionMs: Long,
|
|
||||||
durationMs: Long
|
|
||||||
) {
|
|
||||||
mediaRepository.updateWatchProgress(mediaId, positionMs, durationMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
loadFromCache()
|
|
||||||
ensureReady()
|
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() {
|
private suspend fun loadFromCache() {
|
||||||
val cache = homeCacheDataStore.data.first()
|
val cache = homeCacheDataStore.data.first()
|
||||||
if (cache.suggestions.isNotEmpty()) {
|
if (cache.suggestions.isNotEmpty()) {
|
||||||
_suggestions.value = cache.suggestions.mapNotNull { it.toMedia() }
|
suggestionsState.value = cache.suggestions.mapNotNull { it.toMedia() }
|
||||||
}
|
}
|
||||||
if (cache.continueWatching.isNotEmpty()) {
|
if (cache.continueWatching.isNotEmpty()) {
|
||||||
_continueWatching.value = cache.continueWatching.mapNotNull { it.toMedia() }
|
continueWatchingState.value = cache.continueWatching.mapNotNull { it.toMedia() }
|
||||||
}
|
}
|
||||||
if (cache.nextUp.isNotEmpty()) {
|
if (cache.nextUp.isNotEmpty()) {
|
||||||
_nextUp.value = cache.nextUp.mapNotNull { it.toMedia() }
|
nextUpState.value = cache.nextUp.mapNotNull { it.toMedia() }
|
||||||
}
|
}
|
||||||
if (cache.latestLibraryContent.isNotEmpty()) {
|
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
|
val uuid = runCatching { UUID.fromString(key) }.getOrNull() ?: return@mapNotNull null
|
||||||
uuid to items.mapNotNull { it.toMedia() }
|
uuid to items.mapNotNull { it.toMedia() }
|
||||||
}.toMap()
|
}.toMap()
|
||||||
@@ -114,12 +137,12 @@ class InMemoryAppContentRepository @Inject constructor(
|
|||||||
|
|
||||||
private suspend fun persistHomeCache() {
|
private suspend fun persistHomeCache() {
|
||||||
val cache = HomeCache(
|
val cache = HomeCache(
|
||||||
suggestions = _suggestions.value.map { it.toCachedItem() },
|
suggestions = suggestionsState.value.map { it.toCachedItem() },
|
||||||
continueWatching = _continueWatching.value.map { it.toCachedItem() },
|
continueWatching = continueWatchingState.value.map { it.toCachedItem() },
|
||||||
nextUp = _nextUp.value.map { it.toCachedItem() },
|
nextUp = nextUpState.value.map { it.toCachedItem() },
|
||||||
latestLibraryContent = _latestLibraryContent.value.map { (uuid, items) ->
|
latestLibraryContent = latestLibraryContentState.value.map { (uuid, items) ->
|
||||||
uuid.toString() to items.map { it.toCachedItem() }
|
uuid.toString() to items.map { it.toCachedItem() }
|
||||||
}.toMap()
|
}.toMap(),
|
||||||
)
|
)
|
||||||
homeCacheDataStore.updateData { cache }
|
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() {
|
suspend fun loadLibraries() {
|
||||||
val librariesItem = runCatching { jellyfinApiClient.getLibraries() }
|
val librariesItem = runCatching { jellyfinApiClient.getLibraries() }
|
||||||
.getOrElse { error ->
|
.getOrElse { error ->
|
||||||
Log.w(TAG, "Unable to load libraries", error)
|
Log.w(TAG, "Unable to load libraries", error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//TODO add support for playlists
|
val filteredLibraries = librariesItem.filter {
|
||||||
val filteredLibraries =
|
it.collectionType == CollectionType.MOVIES || it.collectionType == CollectionType.TVSHOWS
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
_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() }
|
val filledLibraries = emptyLibraries.map { loadLibrary(it) }
|
||||||
mediaRepository.upsertMovies(movies)
|
librariesState.value = filledLibraries
|
||||||
|
|
||||||
val series = filledLibraries.filter { it.type == CollectionType.TVSHOWS }.flatMap { it.series.orEmpty() }
|
val movies = filledLibraries.filter { it.type == LibraryKind.MOVIES }.flatMap { it.movies.orEmpty() }
|
||||||
mediaRepository.upsertSeries(series)
|
onlineMediaRepository.upsertMovies(movies)
|
||||||
|
|
||||||
|
val series = filledLibraries.filter { it.type == LibraryKind.SERIES }.flatMap { it.series.orEmpty() }
|
||||||
|
onlineMediaRepository.upsertSeries(series)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadLibrary(library: Library): Library {
|
suspend fun loadLibrary(library: Library): Library {
|
||||||
@@ -197,71 +194,33 @@ class InMemoryAppContentRepository @Inject constructor(
|
|||||||
Log.w(TAG, "Unable to load library ${library.id}", error)
|
Log.w(TAG, "Unable to load library ${library.id}", error)
|
||||||
return library
|
return library
|
||||||
}
|
}
|
||||||
when (library.type) {
|
return when (library.type) {
|
||||||
CollectionType.MOVIES -> {
|
LibraryKind.MOVIES -> library.copy(
|
||||||
val movies = contentItem.map { it.toMovie(serverUrl(), library.id) }
|
movies = contentItem.map { it.toMovie(serverUrl(), library.id) },
|
||||||
return library.copy(movies = movies)
|
)
|
||||||
}
|
LibraryKind.SERIES -> library.copy(
|
||||||
CollectionType.TVSHOWS -> {
|
series = contentItem.map { it.toSeries(serverUrl(), library.id) },
|
||||||
val series = contentItem.map { it.toSeries(serverUrl(), library.id) }
|
)
|
||||||
return library.copy(series = series)
|
|
||||||
}
|
|
||||||
else -> throw UnsupportedOperationException("Unsupported library type: ${library.type}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
suspend fun loadSuggestions() {
|
||||||
val suggestionsItems = runCatching { jellyfinApiClient.getSuggestions() }
|
val suggestionsItems = runCatching { jellyfinApiClient.getSuggestions() }
|
||||||
.getOrElse { error ->
|
.getOrElse { error ->
|
||||||
Log.w(TAG, "Unable to load suggestions", error)
|
Log.w(TAG, "Unable to load suggestions", error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val items = suggestionsItems.mapNotNull { item ->
|
suggestionsState.value = suggestionsItems.mapNotNull { item ->
|
||||||
when (item.type) {
|
when (item.type) {
|
||||||
BaseItemKind.MOVIE -> Media.MovieMedia(movieId = item.id)
|
BaseItemKind.MOVIE -> Media.MovieMedia(movieId = item.id)
|
||||||
BaseItemKind.EPISODE -> Media.EpisodeMedia(
|
BaseItemKind.EPISODE -> Media.EpisodeMedia(episodeId = item.id, seriesId = item.seriesId!!)
|
||||||
episodeId = item.id,
|
|
||||||
seriesId = item.seriesId!!
|
|
||||||
)
|
|
||||||
else -> throw UnsupportedOperationException("Unsupported item type: ${item.type}")
|
else -> throw UnsupportedOperationException("Unsupported item type: ${item.type}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_suggestions.value = items
|
|
||||||
|
|
||||||
//Load episodes, Movies are already loaded at this point
|
|
||||||
suggestionsItems.forEach { item ->
|
suggestionsItems.forEach { item ->
|
||||||
when (item.type) {
|
if (item.type == BaseItemKind.EPISODE) {
|
||||||
BaseItemKind.EPISODE -> {
|
onlineMediaRepository.upsertEpisodes(listOf(item.toEpisode(serverUrl())))
|
||||||
val episode = item.toEpisode(serverUrl())
|
|
||||||
mediaRepository.upsertEpisodes(listOf(episode))
|
|
||||||
}
|
|
||||||
else -> { /* Do nothing */ }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,26 +231,17 @@ class InMemoryAppContentRepository @Inject constructor(
|
|||||||
Log.w(TAG, "Unable to load continue watching", error)
|
Log.w(TAG, "Unable to load continue watching", error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val items = continueWatchingItems.mapNotNull { item ->
|
continueWatchingState.value = continueWatchingItems.mapNotNull { item ->
|
||||||
when (item.type) {
|
when (item.type) {
|
||||||
BaseItemKind.MOVIE -> Media.MovieMedia(movieId = item.id)
|
BaseItemKind.MOVIE -> Media.MovieMedia(movieId = item.id)
|
||||||
BaseItemKind.EPISODE -> Media.EpisodeMedia(
|
BaseItemKind.EPISODE -> Media.EpisodeMedia(episodeId = item.id, seriesId = item.seriesId!!)
|
||||||
episodeId = item.id,
|
|
||||||
seriesId = item.seriesId!!
|
|
||||||
)
|
|
||||||
else -> throw UnsupportedOperationException("Unsupported item type: ${item.type}")
|
else -> throw UnsupportedOperationException("Unsupported item type: ${item.type}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_continueWatching.value = items
|
|
||||||
|
|
||||||
//Load episodes, Movies are already loaded at this point.
|
|
||||||
continueWatchingItems.forEach { item ->
|
continueWatchingItems.forEach { item ->
|
||||||
when (item.type) {
|
if (item.type == BaseItemKind.EPISODE) {
|
||||||
BaseItemKind.EPISODE -> {
|
onlineMediaRepository.upsertEpisodes(listOf(item.toEpisode(serverUrl())))
|
||||||
val episode = item.toEpisode(serverUrl())
|
|
||||||
mediaRepository.upsertEpisodes(listOf(episode))
|
|
||||||
}
|
|
||||||
else -> { /* Do nothing */ }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,90 +252,56 @@ class InMemoryAppContentRepository @Inject constructor(
|
|||||||
Log.w(TAG, "Unable to load next up", error)
|
Log.w(TAG, "Unable to load next up", error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val items = nextUpItems.map { item ->
|
nextUpState.value = nextUpItems.map { item ->
|
||||||
Media.EpisodeMedia(
|
Media.EpisodeMedia(episodeId = item.id, seriesId = item.seriesId!!)
|
||||||
episodeId = item.id,
|
|
||||||
seriesId = item.seriesId!!
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
_nextUp.value = items
|
|
||||||
|
|
||||||
// Load episodes
|
|
||||||
nextUpItems.forEach { item ->
|
nextUpItems.forEach { item ->
|
||||||
val episode = item.toEpisode(serverUrl())
|
onlineMediaRepository.upsertEpisodes(listOf(item.toEpisode(serverUrl())))
|
||||||
mediaRepository.upsertEpisodes(listOf(episode))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadLatestLibraryContent() {
|
suspend fun loadLatestLibraryContent() {
|
||||||
// TODO Make libraries accessible in a field or something that is not this ugly.
|
|
||||||
val librariesItem = runCatching { jellyfinApiClient.getLibraries() }
|
val librariesItem = runCatching { jellyfinApiClient.getLibraries() }
|
||||||
.getOrElse { error ->
|
.getOrElse { error ->
|
||||||
Log.w(TAG, "Unable to load latest library content", error)
|
Log.w(TAG, "Unable to load latest library content", error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val filterLibraries =
|
val filteredLibraries = librariesItem.filter {
|
||||||
librariesItem.filter { it.collectionType == CollectionType.MOVIES || it.collectionType == CollectionType.TVSHOWS }
|
it.collectionType == CollectionType.MOVIES || it.collectionType == CollectionType.TVSHOWS
|
||||||
val latestLibraryContents = filterLibraries.associate { library ->
|
}
|
||||||
|
val latestLibraryContents = filteredLibraries.associate { library ->
|
||||||
val latestFromLibrary = runCatching { jellyfinApiClient.getLatestFromLibrary(library.id) }
|
val latestFromLibrary = runCatching { jellyfinApiClient.getLatestFromLibrary(library.id) }
|
||||||
.getOrElse { error ->
|
.getOrElse { error ->
|
||||||
Log.w(TAG, "Unable to load latest items for library ${library.id}", error)
|
Log.w(TAG, "Unable to load latest items for library ${library.id}", error)
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
library.id to when (library.collectionType) {
|
library.id to when (library.collectionType) {
|
||||||
CollectionType.MOVIES -> {
|
CollectionType.MOVIES -> latestFromLibrary.map {
|
||||||
latestFromLibrary.map {
|
val movie = it.toMovie(serverUrl(), library.id)
|
||||||
val movie = it.toMovie(serverUrl(), library.id)
|
Media.MovieMedia(movieId = movie.id)
|
||||||
Media.MovieMedia(movieId = movie.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
CollectionType.TVSHOWS -> {
|
CollectionType.TVSHOWS -> latestFromLibrary.map {
|
||||||
latestFromLibrary.map {
|
when (it.type) {
|
||||||
when (it.type) {
|
BaseItemKind.SERIES -> {
|
||||||
BaseItemKind.SERIES -> {
|
val series = it.toSeries(serverUrl(), library.id)
|
||||||
val series = it.toSeries(serverUrl(), library.id)
|
Media.SeriesMedia(seriesId = series.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}")
|
|
||||||
}
|
}
|
||||||
|
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}")
|
else -> throw UnsupportedOperationException("Unsupported library type: ${library.collectionType}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_latestLibraryContent.value = latestLibraryContents
|
latestLibraryContentState.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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun serverUrl(): String {
|
private suspend fun serverUrl(): String {
|
||||||
@@ -393,94 +309,75 @@ class InMemoryAppContentRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun BaseItemDto.toLibrary(serverUrl: String): Library {
|
private fun BaseItemDto.toLibrary(serverUrl: String): Library {
|
||||||
return when (this.collectionType) {
|
return when (collectionType) {
|
||||||
CollectionType.MOVIES -> Library(
|
CollectionType.MOVIES -> Library(
|
||||||
id = this.id,
|
id = id,
|
||||||
name = this.name!!,
|
name = name!!,
|
||||||
posterUrl = JellyfinImageHelper.toImageUrl(
|
posterUrl = JellyfinImageHelper.toImageUrl(url = serverUrl, itemId = id, type = ImageType.PRIMARY),
|
||||||
url = serverUrl,
|
type = LibraryKind.MOVIES,
|
||||||
itemId = this.id,
|
movies = emptyList(),
|
||||||
type = ImageType.PRIMARY
|
|
||||||
),
|
|
||||||
type = CollectionType.MOVIES,
|
|
||||||
movies = emptyList()
|
|
||||||
)
|
)
|
||||||
CollectionType.TVSHOWS -> Library(
|
CollectionType.TVSHOWS -> Library(
|
||||||
id = this.id,
|
id = id,
|
||||||
name = this.name!!,
|
name = name!!,
|
||||||
posterUrl = JellyfinImageHelper.toImageUrl(
|
posterUrl = JellyfinImageHelper.toImageUrl(url = serverUrl, itemId = id, type = ImageType.PRIMARY),
|
||||||
url = serverUrl,
|
type = LibraryKind.SERIES,
|
||||||
itemId = this.id,
|
series = emptyList(),
|
||||||
type = ImageType.PRIMARY
|
|
||||||
),
|
|
||||||
type = CollectionType.TVSHOWS,
|
|
||||||
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 {
|
private fun BaseItemDto.toMovie(serverUrl: String, libraryId: UUID): Movie {
|
||||||
return Movie(
|
return Movie(
|
||||||
id = this.id,
|
id = id,
|
||||||
libraryId = libraryId,
|
libraryId = libraryId,
|
||||||
title = this.name ?: "Unknown title",
|
title = name ?: "Unknown title",
|
||||||
progress = this.userData!!.playedPercentage,
|
progress = userData!!.playedPercentage,
|
||||||
watched = this.userData!!.played,
|
watched = userData!!.played,
|
||||||
year = this.productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
|
year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
|
||||||
rating = this.officialRating
|
rating = officialRating ?: "NR",
|
||||||
?: "NR",
|
runtime = formatRuntime(runTimeTicks),
|
||||||
runtime = formatRuntime(this.runTimeTicks),
|
synopsis = overview ?: "No synopsis available",
|
||||||
synopsis = this.overview ?: "No synopsis available",
|
|
||||||
format = container?.uppercase() ?: "VIDEO",
|
format = container?.uppercase() ?: "VIDEO",
|
||||||
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
|
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(url = serverUrl, itemId = id),
|
||||||
url = serverUrl,
|
|
||||||
itemId = this.id,
|
|
||||||
),
|
|
||||||
subtitles = "ENG",
|
subtitles = "ENG",
|
||||||
audioTrack = "ENG",
|
audioTrack = "ENG",
|
||||||
cast = emptyList()
|
cast = emptyList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun BaseItemDto.toSeries(serverUrl: String, libraryId: UUID): Series {
|
private fun BaseItemDto.toSeries(serverUrl: String, libraryId: UUID): Series {
|
||||||
return Series(
|
return Series(
|
||||||
id = this.id,
|
id = id,
|
||||||
libraryId = libraryId,
|
libraryId = libraryId,
|
||||||
name = this.name ?: "Unknown",
|
name = name ?: "Unknown",
|
||||||
synopsis = this.overview ?: "No synopsis available",
|
synopsis = overview ?: "No synopsis available",
|
||||||
year = this.productionYear?.toString()
|
year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
|
||||||
?: this.premiereDate?.year?.toString().orEmpty(),
|
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(url = serverUrl, itemId = id),
|
||||||
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
|
unwatchedEpisodeCount = userData!!.unplayedItemCount!!,
|
||||||
url = serverUrl,
|
seasonCount = childCount!!,
|
||||||
itemId = this.id
|
|
||||||
),
|
|
||||||
unwatchedEpisodeCount = this.userData!!.unplayedItemCount!!,
|
|
||||||
seasonCount = this.childCount!!,
|
|
||||||
seasons = emptyList(),
|
seasons = emptyList(),
|
||||||
cast = emptyList()
|
cast = emptyList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun BaseItemDto.toSeason(serverUrl: String): Season {
|
private fun BaseItemDto.toSeason(): Season {
|
||||||
return Season(
|
return Season(
|
||||||
id = this.id,
|
id = id,
|
||||||
seriesId = this.seriesId!!,
|
seriesId = seriesId!!,
|
||||||
name = this.name ?: "Unknown",
|
name = name ?: "Unknown",
|
||||||
index = this.indexNumber ?: 0,
|
index = indexNumber ?: 0,
|
||||||
unwatchedEpisodeCount = this.userData!!.unplayedItemCount!!,
|
unwatchedEpisodeCount = userData!!.unplayedItemCount!!,
|
||||||
episodeCount = this.childCount!!,
|
episodeCount = childCount!!,
|
||||||
episodes = emptyList()
|
episodes = emptyList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
|
private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
|
||||||
val releaseDate = formatReleaseDate(premiereDate, productionYear)
|
val releaseDate = formatReleaseDate(premiereDate, productionYear)
|
||||||
val imageUrlPrefix = id?.let { itemId ->
|
val imageUrlPrefix = id?.let { itemId ->
|
||||||
JellyfinImageHelper.toPrefixImageUrl(
|
JellyfinImageHelper.toPrefixImageUrl(url = serverUrl, itemId = itemId)
|
||||||
url = serverUrl,
|
|
||||||
itemId = itemId
|
|
||||||
)
|
|
||||||
} ?: ""
|
} ?: ""
|
||||||
return Episode(
|
return Episode(
|
||||||
id = id,
|
id = id,
|
||||||
@@ -496,7 +393,7 @@ class InMemoryAppContentRepository @Inject constructor(
|
|||||||
format = container?.uppercase() ?: "VIDEO",
|
format = container?.uppercase() ?: "VIDEO",
|
||||||
synopsis = overview ?: "No synopsis available.",
|
synopsis = overview ?: "No synopsis available.",
|
||||||
imageUrlPrefix = imageUrlPrefix,
|
imageUrlPrefix = imageUrlPrefix,
|
||||||
cast = emptyList()
|
cast = emptyList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,11 +410,7 @@ class InMemoryAppContentRepository @Inject constructor(
|
|||||||
val totalSeconds = ticks / 10_000_000
|
val totalSeconds = ticks / 10_000_000
|
||||||
val hours = TimeUnit.SECONDS.toHours(totalSeconds)
|
val hours = TimeUnit.SECONDS.toHours(totalSeconds)
|
||||||
val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
|
val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
|
||||||
return if (hours > 0) {
|
return if (hours > 0) "${hours}h ${minutes}m" else "${minutes}m"
|
||||||
"${hours}h ${minutes}m"
|
|
||||||
} else {
|
|
||||||
"${minutes}m"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import hu.bbara.purefin.core.model.Episode
|
|||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.core.model.Season
|
import hu.bbara.purefin.core.model.Season
|
||||||
import hu.bbara.purefin.core.model.Series
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -19,74 +26,41 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
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
|
@Singleton
|
||||||
class InMemoryMediaRepository @Inject constructor(
|
class InMemoryMediaRepository @Inject constructor(
|
||||||
private val userSessionRepository: UserSessionRepository,
|
private val userSessionRepository: UserSessionRepository,
|
||||||
private val jellyfinApiClient: JellyfinApiClient,
|
private val jellyfinApiClient: JellyfinApiClient,
|
||||||
) : MediaRepository {
|
) : MediaCatalogReader, MediaProgressWriter {
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
override val ready: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
private val moviesState = MutableStateFlow<Map<UUID, Movie>>(emptyMap())
|
||||||
|
override val movies: StateFlow<Map<UUID, Movie>> = moviesState.asStateFlow()
|
||||||
|
|
||||||
internal val _movies: MutableStateFlow<Map<UUID, Movie>> = MutableStateFlow(emptyMap())
|
private val seriesState = MutableStateFlow<Map<UUID, Series>>(emptyMap())
|
||||||
override val movies: StateFlow<Map<UUID, Movie>> = _movies.asStateFlow()
|
override val series: StateFlow<Map<UUID, Series>> = seriesState.asStateFlow()
|
||||||
|
|
||||||
internal val _series: MutableStateFlow<Map<UUID, Series>> = MutableStateFlow(emptyMap())
|
private val episodesState = MutableStateFlow<Map<UUID, Episode>>(emptyMap())
|
||||||
override val series: StateFlow<Map<UUID, Series>> = _series.asStateFlow()
|
override val episodes: StateFlow<Map<UUID, Episode>> = episodesState.asStateFlow()
|
||||||
|
|
||||||
internal val _episodes: MutableStateFlow<Map<UUID, Episode>> = MutableStateFlow(emptyMap())
|
fun upsertMovies(movies: List<Movie>) {
|
||||||
override val episodes: StateFlow<Map<UUID, Episode>> = _episodes.asStateFlow()
|
moviesState.update { current -> current + movies.associateBy { it.id } }
|
||||||
override fun upsertMovies(movies: List<Movie>) {
|
|
||||||
_movies.update { current -> current + movies.associateBy { it.id } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun upsertSeries(series: List<Series>) {
|
fun upsertSeries(series: List<Series>) {
|
||||||
_series.update { current -> current + series.associateBy { it.id } }
|
seriesState.update { current -> current + series.associateBy { it.id } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun upsertEpisodes(episodes: List<Episode>) {
|
fun upsertEpisodes(episodes: List<Episode>) {
|
||||||
_episodes.update { current -> current + episodes.associateBy { it.id } }
|
episodesState.update { current -> current + episodes.associateBy { it.id } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
ensureSeriesContentLoaded(seriesId)
|
ensureSeriesContentLoaded(seriesId)
|
||||||
}
|
}
|
||||||
return _series.map { it[seriesId] }
|
return seriesState.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 } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
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 progressPercent = (positionMs.toDouble() / durationMs.toDouble()) * 100.0
|
||||||
val watched = progressPercent >= 90.0
|
val watched = progressPercent >= 90.0
|
||||||
|
|
||||||
if (_movies.value.containsKey(mediaId)) {
|
if (moviesState.value.containsKey(mediaId)) {
|
||||||
_movies.update { current ->
|
moviesState.update { current ->
|
||||||
val movie = current[mediaId] ?: return@update current
|
val movie = current[mediaId] ?: return@update current
|
||||||
current + (mediaId to movie.copy(progress = progressPercent, watched = watched))
|
current + (mediaId to movie.copy(progress = progressPercent, watched = watched))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (_episodes.value.containsKey(mediaId)) {
|
if (episodesState.value.containsKey(mediaId)) {
|
||||||
_episodes.update { current ->
|
episodesState.update { current ->
|
||||||
val episode = current[mediaId] ?: return@update current
|
val episode = current[mediaId] ?: return@update current
|
||||||
current + (mediaId to episode.copy(progress = progressPercent, watched = watched))
|
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(
|
return Season(
|
||||||
id = this.id,
|
id = id,
|
||||||
seriesId = this.seriesId!!,
|
seriesId = seriesId!!,
|
||||||
name = this.name ?: "Unknown",
|
name = name ?: "Unknown",
|
||||||
index = this.indexNumber ?: 0,
|
index = indexNumber ?: 0,
|
||||||
unwatchedEpisodeCount = this.userData!!.unplayedItemCount!!,
|
unwatchedEpisodeCount = userData!!.unplayedItemCount!!,
|
||||||
episodeCount = this.childCount!!,
|
episodeCount = childCount!!,
|
||||||
episodes = emptyList()
|
episodes = emptyList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +133,7 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
format = container?.uppercase() ?: "VIDEO",
|
format = container?.uppercase() ?: "VIDEO",
|
||||||
synopsis = overview ?: "No synopsis available.",
|
synopsis = overview ?: "No synopsis available.",
|
||||||
imageUrlPrefix = imageUrlPrefix,
|
imageUrlPrefix = imageUrlPrefix,
|
||||||
cast = emptyList()
|
cast = emptyList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,23 +3,13 @@ package hu.bbara.purefin.core.data
|
|||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.core.model.Series
|
import hu.bbara.purefin.core.model.Series
|
||||||
|
import java.util.UUID
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
interface MediaRepository {
|
interface MediaCatalogReader {
|
||||||
val ready: StateFlow<Boolean>
|
|
||||||
val movies: StateFlow<Map<UUID, Movie>>
|
val movies: StateFlow<Map<UUID, Movie>>
|
||||||
val series: StateFlow<Map<UUID, Series>>
|
val series: StateFlow<Map<UUID, Series>>
|
||||||
val episodes: StateFlow<Map<UUID, Episode>>
|
val episodes: StateFlow<Map<UUID, Episode>>
|
||||||
fun upsertMovies(movies: List<Movie>) {
|
|
||||||
}
|
|
||||||
fun upsertSeries(series: List<Series>) {
|
|
||||||
}
|
|
||||||
fun upsertEpisodes(episodes: List<Episode>) {
|
|
||||||
}
|
|
||||||
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?>
|
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?>
|
||||||
fun setReady() {
|
|
||||||
}
|
|
||||||
suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long)
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user