refactor: update package structure and imports for consistency across UI components

This commit is contained in:
2026-04-14 22:03:36 +02:00
parent 1e6c0e838e
commit b67c8076f1
146 changed files with 1239 additions and 1267 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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),

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")
} }

View File

@@ -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"

View File

@@ -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>,

View File

@@ -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)
} }

View File

@@ -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,

View File

@@ -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(

View File

@@ -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() }

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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)
)
}
}

View File

@@ -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,
))
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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() }

View File

@@ -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> {

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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),

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}")
} }

View File

@@ -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(

View File

@@ -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,

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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(

View File

@@ -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
} }
} }

View File

@@ -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,

View File

@@ -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

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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()
} }
} }

View File

@@ -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()
}
}

View File

@@ -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
} }

View File

@@ -0,0 +1,7 @@
package hu.bbara.purefin.core.data
import java.util.UUID
interface EpisodeSeriesLookup {
suspend fun preferenceKeyFor(mediaId: UUID): String
}

View File

@@ -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>>

View File

@@ -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 {

View File

@@ -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(),
) )
} }

View File

@@ -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)
}
}

View File

@@ -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