mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat: introduce core data models and interfaces for playback and downloads
This commit is contained in:
@@ -52,9 +52,19 @@ kotlin {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
implementation(project(":core:image"))
|
||||
implementation(project(":core:data"))
|
||||
implementation(project(":core:navigation"))
|
||||
implementation(project(":core:download"))
|
||||
implementation(project(":core:player"))
|
||||
implementation(project(":feature:shared"))
|
||||
implementation(project(":data:jellyfin"))
|
||||
implementation(project(":data:offline"))
|
||||
implementation(project(":data:catalog"))
|
||||
implementation(project(":feature:browse"))
|
||||
implementation(project(":feature:search"))
|
||||
implementation(project(":feature:content"))
|
||||
implementation(project(":feature:downloads"))
|
||||
implementation(project(":feature:login"))
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
@@ -83,8 +93,6 @@ dependencies {
|
||||
implementation(libs.media3.datasource.okhttp)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -18,10 +18,10 @@ import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import hu.bbara.purefin.tv.home.TvHomeScreen
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.junit.Assert.assertEquals
|
||||
|
||||
@@ -20,13 +20,13 @@ import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection
|
||||
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
|
||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox
|
||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.tv.navigation.LocalNavigationManager
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import hu.bbara.purefin.core.navigation.Route
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.feature.content.episode.EpisodeScreenViewModel
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
|
||||
@Composable
|
||||
fun TvEpisodeScreen(
|
||||
@@ -87,7 +87,7 @@ internal fun TvEpisodeScreenContent(
|
||||
) {
|
||||
item {
|
||||
TvMediaDetailBodyBox(
|
||||
backgroundImageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
backgroundImageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||
modifier = it
|
||||
) {
|
||||
TvEpisodeHeroSection(
|
||||
|
||||
@@ -21,9 +21,9 @@ import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
|
||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox
|
||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
||||
import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl
|
||||
import hu.bbara.purefin.feature.shared.navigation.MovieDto
|
||||
import hu.bbara.purefin.core.navigation.MovieDto
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
||||
import hu.bbara.purefin.feature.content.movie.MovieScreenViewModel
|
||||
|
||||
@Composable
|
||||
fun TvMovieScreen(
|
||||
|
||||
@@ -59,13 +59,13 @@ import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
||||
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.model.CastMember
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Season
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.feature.content.series.SeriesViewModel
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
|
||||
internal const val SeriesPlayButtonTag = "series-play-button"
|
||||
internal const val SeriesFirstSeasonTabTag = "series-first-season-tab"
|
||||
@@ -296,7 +296,7 @@ private fun TvEpisodeCard(
|
||||
)
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
model = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
|
||||
@@ -22,10 +22,10 @@ import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
|
||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox
|
||||
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
|
||||
import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl
|
||||
import hu.bbara.purefin.feature.shared.navigation.SeriesDto
|
||||
import hu.bbara.purefin.core.navigation.SeriesDto
|
||||
import hu.bbara.purefin.core.model.Season
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
|
||||
import hu.bbara.purefin.feature.content.series.SeriesViewModel
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -32,8 +32,8 @@ import androidx.compose.ui.unit.sp
|
||||
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.FocusableItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import java.util.UUID
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
|
||||
internal val MediaDetailHorizontalPadding = 48.dp
|
||||
private val MediaDetailHeaderTopPadding = 104.dp
|
||||
@@ -151,8 +151,8 @@ internal fun TvMediaDetailBodyBox(
|
||||
}
|
||||
|
||||
internal fun tvMediaDetailBackgroundImageUrl(imageUrlPrefix: String?): String {
|
||||
val primaryImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
||||
val backdropImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.BACKDROP)
|
||||
val primaryImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||
val backdropImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.BACKDROP)
|
||||
return backdropImageUrl.ifBlank { primaryImageUrl }
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ import hu.bbara.purefin.common.ui.PurefinComplexTextField
|
||||
import hu.bbara.purefin.common.ui.PurefinPasswordField
|
||||
import hu.bbara.purefin.common.ui.PurefinTextButton
|
||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.feature.shared.login.LoginViewModel
|
||||
import hu.bbara.purefin.feature.login.LoginViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -37,13 +37,13 @@ import coil3.request.crossfade
|
||||
import coil3.util.DebugLogger
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
|
||||
import hu.bbara.purefin.core.data.SessionBootstrapper
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinAuthInterceptor
|
||||
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.core.navigation.NavigationCommand
|
||||
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||
import hu.bbara.purefin.core.navigation.Route
|
||||
import hu.bbara.purefin.tv.navigation.LocalNavigationBackStack
|
||||
import hu.bbara.purefin.tv.navigation.LocalNavigationManager
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
@@ -66,7 +66,7 @@ class TvActivity : ComponentActivity() {
|
||||
lateinit var navigationManager: NavigationManager
|
||||
|
||||
@Inject
|
||||
lateinit var jellyfinApiClient: JellyfinApiClient
|
||||
lateinit var sessionBootstrapper: SessionBootstrapper
|
||||
|
||||
@Inject
|
||||
lateinit var authInterceptor: JellyfinAuthInterceptor
|
||||
@@ -94,7 +94,7 @@ class TvActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
private suspend fun init() {
|
||||
jellyfinApiClient.updateApiClient()
|
||||
sessionBootstrapper.initialize()
|
||||
}
|
||||
|
||||
private fun configureImageLoader() {
|
||||
|
||||
@@ -15,7 +15,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import hu.bbara.purefin.feature.shared.home.AppViewModel
|
||||
import hu.bbara.purefin.feature.browse.home.AppViewModel
|
||||
import hu.bbara.purefin.tv.home.TvHomeScreen
|
||||
import hu.bbara.purefin.tv.home.ui.TvDrawerDestination
|
||||
import hu.bbara.purefin.tv.home.ui.TvDrawerDestinationItem
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package hu.bbara.purefin.tv.di
|
||||
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.data.download.MediaDownloadController
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import java.util.UUID
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object TvPlaybackModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePlaybackDataSourceFactory(
|
||||
okHttpDataSourceFactory: OkHttpDataSource.Factory
|
||||
): DataSource.Factory {
|
||||
return okHttpDataSourceFactory
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMediaDownloadController(): MediaDownloadController {
|
||||
return TvNoOpMediaDownloadController
|
||||
}
|
||||
}
|
||||
|
||||
internal object TvNoOpMediaDownloadController : MediaDownloadController {
|
||||
private val notDownloaded = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
|
||||
|
||||
override fun observeActiveDownloads(): Flow<Map<String, Float>> = flowOf(emptyMap())
|
||||
|
||||
override fun observeDownloadState(contentId: String) = notDownloaded
|
||||
|
||||
override suspend fun downloadMovie(movieId: UUID) = Unit
|
||||
|
||||
override suspend fun cancelDownload(movieId: UUID) = Unit
|
||||
|
||||
override suspend fun downloadEpisode(episodeId: UUID) = Unit
|
||||
|
||||
override suspend fun downloadEpisodes(episodeIds: List<UUID>) = Unit
|
||||
|
||||
override suspend fun cancelEpisodeDownload(episodeId: UUID) = Unit
|
||||
|
||||
override suspend fun enableSmartDownload(seriesId: UUID) = Unit
|
||||
|
||||
override suspend fun syncSmartDownloads() = Unit
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import hu.bbara.purefin.core.data.client.PlaybackProfileFamily
|
||||
import hu.bbara.purefin.core.data.PlaybackProfileFamily
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
|
||||
@@ -9,10 +9,10 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import hu.bbara.purefin.tv.home.ui.TvFocusedItemHero
|
||||
import hu.bbara.purefin.tv.home.ui.TvHomeContent
|
||||
import hu.bbara.purefin.tv.home.ui.rememberTvHomeHeroState
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package hu.bbara.purefin.tv.home.ui
|
||||
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import java.util.UUID
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
internal data class TvFocusedHeroModel(
|
||||
@@ -42,7 +42,7 @@ internal fun FocusableItem.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
||||
}
|
||||
|
||||
internal fun backdropImageUrl(imageUrlPrefix: String?, fallbackImageUrl: String): String {
|
||||
val backdropImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.BACKDROP)
|
||||
val backdropImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.BACKDROP)
|
||||
return backdropImageUrl.ifBlank { fallbackImageUrl }
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ private fun Movie.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
||||
id = id,
|
||||
backdropImageUrl = backdropImageUrl(
|
||||
imageUrlPrefix = imageUrlPrefix,
|
||||
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
||||
fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||
),
|
||||
title = title,
|
||||
metadata = listOf(year, rating, runtime, format).compactMetadata(),
|
||||
@@ -63,7 +63,7 @@ private fun Episode.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
||||
id = id,
|
||||
backdropImageUrl = backdropImageUrl(
|
||||
imageUrlPrefix = imageUrlPrefix,
|
||||
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
||||
fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||
),
|
||||
title = title,
|
||||
metadata = listOf(
|
||||
@@ -87,7 +87,7 @@ private fun Series.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
||||
id = id,
|
||||
backdropImageUrl = backdropImageUrl(
|
||||
imageUrlPrefix = imageUrlPrefix,
|
||||
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
||||
fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||
),
|
||||
title = name,
|
||||
metadata = listOf(year, seasonLabel(seasonCount), unwatchedText).compactMetadata(),
|
||||
|
||||
@@ -24,11 +24,11 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import java.util.UUID
|
||||
|
||||
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
||||
|
||||
@@ -5,11 +5,11 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import java.util.UUID
|
||||
|
||||
internal data class TvHomeItemRegistry(
|
||||
|
||||
@@ -47,14 +47,14 @@ import androidx.compose.ui.unit.sp
|
||||
import hu.bbara.purefin.common.ui.PosterCard
|
||||
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import java.util.UUID
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
|
||||
private val TvHomeSectionsThumbShape = RoundedCornerShape(20.dp)
|
||||
private val TvHomeSectionsHorizontalPadding = 32.dp
|
||||
@@ -93,14 +93,14 @@ fun TvContinueWatchingSection(
|
||||
title = item.primaryText,
|
||||
supporting = item.secondaryText,
|
||||
imageUrl = when (item.type) {
|
||||
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||
MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl(
|
||||
prefixImageUrl = item.movie?.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
artworkKind = ArtworkKind.PRIMARY
|
||||
)
|
||||
|
||||
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
||||
MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl(
|
||||
prefixImageUrl = item.episode?.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
artworkKind = ArtworkKind.PRIMARY
|
||||
)
|
||||
|
||||
else -> null
|
||||
|
||||
@@ -44,7 +44,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.core.model.LibraryKind
|
||||
|
||||
internal const val TvLibrariesOverviewItemTagPrefix = "tv-libraries-overview-item-"
|
||||
|
||||
@@ -24,9 +24,9 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.common.ui.PosterCard
|
||||
import hu.bbara.purefin.common.ui.components.PurefinIconButton
|
||||
import hu.bbara.purefin.feature.shared.navigation.LibraryDto
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
||||
import hu.bbara.purefin.core.navigation.LibraryDto
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.library.LibraryViewModel
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -3,8 +3,8 @@ 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
|
||||
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||
import hu.bbara.purefin.core.navigation.Route
|
||||
|
||||
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
||||
staticCompositionLocalOf { error("NavigationManager not provided") }
|
||||
|
||||
@@ -6,7 +6,7 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityRetainedComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import hu.bbara.purefin.core.navigation.Route
|
||||
|
||||
@Module
|
||||
@InstallIn(ActivityRetainedComponent::class)
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.navigation3.runtime.EntryProviderScope
|
||||
import hu.bbara.purefin.app.content.episode.TvEpisodeScreen
|
||||
import hu.bbara.purefin.app.content.movie.TvMovieScreen
|
||||
import hu.bbara.purefin.app.content.series.TvSeriesScreen
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import hu.bbara.purefin.core.navigation.Route
|
||||
import hu.bbara.purefin.login.ui.LoginScreen
|
||||
import hu.bbara.purefin.tv.TvAppScreen
|
||||
import hu.bbara.purefin.tv.library.ui.TvLibraryScreen
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package hu.bbara.purefin.tv.di
|
||||
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class TvNoOpMediaDownloadControllerTest {
|
||||
|
||||
@Test
|
||||
fun `tv no-op download controller always reports no downloads`() = runBlocking {
|
||||
assertEquals(emptyMap<String, Float>(), TvNoOpMediaDownloadController.observeActiveDownloads().first())
|
||||
assertEquals(
|
||||
DownloadState.NotDownloaded,
|
||||
TvNoOpMediaDownloadController.observeDownloadState("movie-1").value
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package hu.bbara.purefin.tv.home.ui
|
||||
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
@@ -33,7 +33,7 @@ class TvFocusedItemHeroTest {
|
||||
assertEquals("42%", hero.progressLabel)
|
||||
assertNull(hero.watchedText)
|
||||
assertEquals(
|
||||
JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.BACKDROP),
|
||||
ImageUrlBuilder.finishImageUrl(movie.imageUrlPrefix, ArtworkKind.BACKDROP),
|
||||
hero.backdropImageUrl
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package hu.bbara.purefin.tv.home.ui
|
||||
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.junit.Assert.assertEquals
|
||||
|
||||
@@ -51,10 +51,19 @@ kotlin {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
implementation(project(":core:image"))
|
||||
implementation(project(":core:data"))
|
||||
implementation(project(":feature:download"))
|
||||
implementation(project(":core:navigation"))
|
||||
implementation(project(":core:download"))
|
||||
implementation(project(":core:player"))
|
||||
implementation(project(":feature:shared"))
|
||||
implementation(project(":data:jellyfin"))
|
||||
implementation(project(":data:offline"))
|
||||
implementation(project(":data:catalog"))
|
||||
implementation(project(":feature:browse"))
|
||||
implementation(project(":feature:search"))
|
||||
implementation(project(":feature:content"))
|
||||
implementation(project(":feature:downloads"))
|
||||
implementation(project(":feature:login"))
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
@@ -83,8 +92,6 @@ dependencies {
|
||||
implementation(libs.media3.datasource.okhttp)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.Purefin" />
|
||||
<service
|
||||
android:name="hu.bbara.purefin.feature.download.PurefinDownloadService"
|
||||
android:name="hu.bbara.purefin.core.download.PurefinDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
</application>
|
||||
|
||||
@@ -36,15 +36,15 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import coil3.request.crossfade
|
||||
import coil3.util.DebugLogger
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
|
||||
import hu.bbara.purefin.core.data.SessionBootstrapper
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinAuthInterceptor
|
||||
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.navigation.LocalNavigationBackStack
|
||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||
import hu.bbara.purefin.core.navigation.NavigationCommand
|
||||
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||
import hu.bbara.purefin.core.navigation.Route
|
||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import hu.bbara.purefin.ui.theme.backgroundDark
|
||||
@@ -65,7 +65,7 @@ class PurefinActivity : ComponentActivity() {
|
||||
lateinit var navigationManager: NavigationManager
|
||||
|
||||
@Inject
|
||||
lateinit var jellyfinApiClient: JellyfinApiClient
|
||||
lateinit var sessionBootstrapper: SessionBootstrapper
|
||||
|
||||
@Inject
|
||||
lateinit var authInterceptor: JellyfinAuthInterceptor
|
||||
@@ -93,7 +93,7 @@ class PurefinActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
private suspend fun init() {
|
||||
jellyfinApiClient.updateApiClient()
|
||||
sessionBootstrapper.initialize()
|
||||
}
|
||||
|
||||
private fun configureImageLoader() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityRetainedComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import hu.bbara.purefin.core.navigation.Route
|
||||
import hu.bbara.purefin.navigation.appRouteEntryBuilder
|
||||
|
||||
@Module
|
||||
|
||||
@@ -4,7 +4,7 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import hu.bbara.purefin.core.data.client.PlaybackProfileFamily
|
||||
import hu.bbara.purefin.core.data.PlaybackProfileFamily
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
|
||||
@@ -3,8 +3,8 @@ 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
|
||||
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||
import hu.bbara.purefin.core.navigation.Route
|
||||
|
||||
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
||||
staticCompositionLocalOf { error("NavigationManager not provided") }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package hu.bbara.purefin.navigation
|
||||
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import hu.bbara.purefin.core.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
|
||||
|
||||
@@ -27,7 +27,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.feature.shared.search.SearchViewModel
|
||||
import hu.bbara.purefin.feature.search.SearchViewModel
|
||||
|
||||
@Composable
|
||||
fun DefaultTopBar(
|
||||
|
||||
@@ -23,7 +23,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.request.ImageRequest
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator
|
||||
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
|
||||
@@ -10,7 +10,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import hu.bbara.purefin.feature.shared.home.AppViewModel
|
||||
import hu.bbara.purefin.feature.browse.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
|
||||
|
||||
@@ -27,7 +27,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.feature.shared.download.ActiveDownloadItem
|
||||
import hu.bbara.purefin.feature.downloads.ActiveDownloadItem
|
||||
|
||||
@Composable
|
||||
internal fun DownloadingItemRow(
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||
import hu.bbara.purefin.feature.shared.download.DownloadsViewModel
|
||||
import hu.bbara.purefin.feature.downloads.DownloadsViewModel
|
||||
|
||||
@Composable
|
||||
fun DownloadsContent(
|
||||
|
||||
@@ -37,19 +37,19 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.ui.common.media.MediaMetaChip
|
||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.core.download.DownloadState
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import hu.bbara.purefin.core.navigation.Route
|
||||
import hu.bbara.purefin.core.model.CastMember
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
||||
import hu.bbara.purefin.feature.content.episode.EpisodeScreenViewModel
|
||||
import hu.bbara.purefin.ui.screen.episode.components.EpisodeDetails
|
||||
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBar
|
||||
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBarShortcut
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
@@ -167,7 +167,7 @@ private fun EpisodeHeroSection(
|
||||
.height(sectionHeight)
|
||||
) {
|
||||
MediaHero(
|
||||
imageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
imageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||
backgroundColor = scheme.background,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||
import hu.bbara.purefin.ui.common.button.MediaActionButton
|
||||
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
||||
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.download.DownloadState
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.player.PlayerActivity
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ import androidx.compose.ui.Modifier
|
||||
import hu.bbara.purefin.ui.screen.home.components.HomeContent
|
||||
import hu.bbara.purefin.ui.screen.home.components.HomeTopBar
|
||||
import hu.bbara.purefin.ui.screen.home.components.search.HomeSearchOverlay
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.SuggestedItem
|
||||
import hu.bbara.purefin.ui.screen.AppBottomBar
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.SuggestedItem
|
||||
import hu.bbara.purefin.ui.screen.home.components.continuewatching.ContinueWatchingSection
|
||||
import hu.bbara.purefin.ui.screen.home.components.featured.SuggestionsSection
|
||||
import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection
|
||||
|
||||
@@ -25,11 +25,11 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import java.util.UUID
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
|
||||
@Composable
|
||||
internal fun ContinueWatchingCard(
|
||||
@@ -53,13 +53,13 @@ internal fun ContinueWatchingCard(
|
||||
else -> ""
|
||||
}
|
||||
val imageUrl = when (item.type) {
|
||||
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
||||
MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl(
|
||||
prefixImageUrl = item.movie?.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
artworkKind = ArtworkKind.PRIMARY
|
||||
)
|
||||
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
||||
MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl(
|
||||
prefixImageUrl = item.episode?.imageUrlPrefix,
|
||||
imageType = ImageType.PRIMARY
|
||||
artworkKind = ArtworkKind.PRIMARY
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
||||
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -26,7 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
||||
import hu.bbara.purefin.feature.browse.home.SuggestedItem
|
||||
|
||||
@Composable
|
||||
internal fun SuggestionCard(
|
||||
|
||||
@@ -22,7 +22,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
||||
import hu.bbara.purefin.feature.browse.home.SuggestedItem
|
||||
|
||||
@Composable
|
||||
fun SuggestionsSection(
|
||||
|
||||
@@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator
|
||||
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import java.util.UUID
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import java.util.UUID
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
|
||||
@Composable
|
||||
internal fun NextUpCard(
|
||||
@@ -57,7 +57,7 @@ internal fun NextUpCard(
|
||||
.background(scheme.surface)
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
model = JellyfinImageHelper.finishImageUrl(item.episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
model = ImageUrlBuilder.finishImageUrl(item.episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||
contentDescription = item.primaryText,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
|
||||
@@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
||||
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -35,7 +35,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.feature.shared.search.SearchViewModel
|
||||
import hu.bbara.purefin.feature.search.SearchViewModel
|
||||
import java.util.UUID
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.core.model.MediaKind
|
||||
import hu.bbara.purefin.feature.shared.search.SearchResult
|
||||
import hu.bbara.purefin.feature.search.SearchResult
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -23,9 +23,9 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||
import hu.bbara.purefin.ui.common.button.PurefinIconButton
|
||||
import hu.bbara.purefin.feature.shared.navigation.LibraryDto
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
||||
import hu.bbara.purefin.core.navigation.LibraryDto
|
||||
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||
import hu.bbara.purefin.feature.browse.library.LibraryViewModel
|
||||
|
||||
@Composable
|
||||
fun LibraryScreen(
|
||||
|
||||
@@ -41,7 +41,7 @@ import hu.bbara.purefin.ui.common.textfield.PurefinComplexTextField
|
||||
import hu.bbara.purefin.ui.common.textfield.PurefinPasswordField
|
||||
import hu.bbara.purefin.ui.common.button.PurefinTextButton
|
||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.feature.shared.login.LoginViewModel
|
||||
import hu.bbara.purefin.feature.login.LoginViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -35,16 +35,16 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.ui.common.media.MediaMetaChip
|
||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.feature.shared.navigation.MovieDto
|
||||
import hu.bbara.purefin.core.download.DownloadState
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.navigation.MovieDto
|
||||
import hu.bbara.purefin.core.model.CastMember
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel
|
||||
import hu.bbara.purefin.feature.content.movie.MovieScreenViewModel
|
||||
import hu.bbara.purefin.ui.screen.movie.components.MovieDetails
|
||||
import hu.bbara.purefin.ui.screen.movie.components.MovieTopBar
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
@@ -139,7 +139,7 @@ fun MediaHeroSection(
|
||||
.height(sectionHeight)
|
||||
) {
|
||||
MediaHero(
|
||||
imageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
|
||||
imageUrl = ImageUrlBuilder.finishImageUrl(movie.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||
backgroundColor = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ import hu.bbara.purefin.ui.common.button.GhostIconButton
|
||||
import hu.bbara.purefin.ui.common.button.MediaActionButton
|
||||
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
||||
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.download.DownloadState
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.player.PlayerActivity
|
||||
|
||||
|
||||
@@ -33,14 +33,14 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import hu.bbara.purefin.ui.common.media.MediaSynopsis
|
||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.feature.shared.navigation.SeriesDto
|
||||
import hu.bbara.purefin.core.download.DownloadState
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.navigation.SeriesDto
|
||||
import hu.bbara.purefin.core.model.CastMember
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Season
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
|
||||
import hu.bbara.purefin.feature.content.series.SeriesViewModel
|
||||
import hu.bbara.purefin.ui.screen.series.components.CastRow
|
||||
import hu.bbara.purefin.ui.screen.series.components.EpisodeCarousel
|
||||
import hu.bbara.purefin.ui.screen.series.components.SeasonTabs
|
||||
@@ -49,7 +49,7 @@ import hu.bbara.purefin.ui.screen.series.components.SeriesDownloadOption
|
||||
import hu.bbara.purefin.ui.screen.series.components.SeriesMetaChips
|
||||
import hu.bbara.purefin.ui.screen.series.components.SeriesTopBar
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
@@ -207,7 +207,7 @@ private fun SeriesHeroSection(
|
||||
.height(sectionHeight)
|
||||
) {
|
||||
MediaHero(
|
||||
imageUrl = JellyfinImageHelper.finishImageUrl(series.imageUrlPrefix, ImageType.PRIMARY),
|
||||
imageUrl = ImageUrlBuilder.finishImageUrl(series.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||
backgroundColor = scheme.background,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
@@ -65,18 +65,18 @@ import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.core.download.DownloadState
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
||||
import hu.bbara.purefin.core.navigation.Route
|
||||
import hu.bbara.purefin.core.model.CastMember
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Season
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import hu.bbara.purefin.feature.shared.content.series.SeriesViewModel
|
||||
import hu.bbara.purefin.feature.content.series.SeriesViewModel
|
||||
import hu.bbara.purefin.player.PlayerActivity
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import hu.bbara.purefin.core.image.ArtworkKind
|
||||
|
||||
@Composable
|
||||
internal fun SeriesTopBar(
|
||||
@@ -349,7 +349,7 @@ private fun EpisodeCard(
|
||||
.border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp))
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
||||
model = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
|
||||
@@ -3,9 +3,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -30,15 +27,7 @@ kotlin {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
implementation(libs.jellyfin.core)
|
||||
implementation(libs.media3.common)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.hilt)
|
||||
ksp(libs.hilt.compiler)
|
||||
implementation(libs.datastore)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.logging.interceptor)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
interface AuthenticationRepository {
|
||||
suspend fun login(url: String, username: String, password: String): Boolean
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.core.model.Season
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import java.util.UUID
|
||||
|
||||
interface DownloadMediaSourceResolver {
|
||||
suspend fun resolveMovieDownload(movieId: UUID): MovieDownloadSource?
|
||||
suspend fun resolveEpisodeDownload(episodeId: UUID): EpisodeDownloadSource?
|
||||
suspend fun isEpisodeWatched(episodeId: UUID): Boolean
|
||||
suspend fun getUnwatchedEpisodeIds(
|
||||
seriesId: UUID,
|
||||
excludedEpisodeIds: Set<UUID>,
|
||||
limit: Int,
|
||||
): List<UUID>
|
||||
}
|
||||
|
||||
data class MovieDownloadSource(
|
||||
val movie: Movie,
|
||||
val playbackUrl: String,
|
||||
val customCacheKey: String?,
|
||||
)
|
||||
|
||||
data class EpisodeDownloadSource(
|
||||
val episode: Episode,
|
||||
val series: Series,
|
||||
val season: Season,
|
||||
val playbackUrl: String,
|
||||
val customCacheKey: String?,
|
||||
)
|
||||
@@ -1,16 +0,0 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class NetworkModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindNetworkMonitor(impl: ConnectivityNetworkMonitor): NetworkMonitor
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
enum class PlaybackMethod {
|
||||
DIRECT_PLAY,
|
||||
DIRECT_STREAM,
|
||||
TRANSCODE,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
enum class PlaybackProfileFamily {
|
||||
MOBILE,
|
||||
TV,
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
import hu.bbara.purefin.core.data.client.PlaybackReportContext
|
||||
import java.util.UUID
|
||||
|
||||
interface PlaybackProgressReporter {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
data class PlaybackReportContext(
|
||||
val playMethod: PlaybackMethod,
|
||||
val mediaSourceId: String?,
|
||||
val audioStreamIndex: Int?,
|
||||
val subtitleStreamIndex: Int?,
|
||||
val liveStreamId: String?,
|
||||
val playSessionId: String?,
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
interface SessionBootstrapper {
|
||||
suspend fun initialize()
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package hu.bbara.purefin.core.data.client
|
||||
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.PlayMethod
|
||||
|
||||
data class PlaybackDecision(
|
||||
val url: String,
|
||||
val mediaSource: MediaSourceInfo,
|
||||
val reportContext: PlaybackReportContext
|
||||
)
|
||||
|
||||
data class PlaybackReportContext(
|
||||
val playMethod: PlayMethod,
|
||||
val mediaSourceId: String?,
|
||||
val audioStreamIndex: Int?,
|
||||
val subtitleStreamIndex: Int?,
|
||||
val liveStreamId: String?,
|
||||
val playSessionId: String?,
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
package hu.bbara.purefin.core.data.domain.usecase
|
||||
|
||||
import hu.bbara.purefin.core.data.HomeRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class RefreshHomeDataUseCase @Inject constructor(
|
||||
private val repository: HomeRepository
|
||||
) {
|
||||
suspend operator fun invoke() {
|
||||
repository.refreshHomeData()
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package hu.bbara.purefin.core.data.image
|
||||
|
||||
import java.util.UUID
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
class JellyfinImageHelper {
|
||||
companion object {
|
||||
|
||||
fun toImageUrl(url: String, itemId: UUID, type: ImageType): String {
|
||||
if (url.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
return StringBuilder()
|
||||
.append(url)
|
||||
.append("/Items/")
|
||||
.append(itemId)
|
||||
.append("/Images/")
|
||||
.append(type.serialName)
|
||||
.toString()
|
||||
}
|
||||
|
||||
fun toPrefixImageUrl(url: String, itemId: UUID): String {
|
||||
if (url.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
return StringBuilder()
|
||||
.append(url)
|
||||
.append("/Items/")
|
||||
.append(itemId)
|
||||
.append("/Images/")
|
||||
.toString()
|
||||
}
|
||||
|
||||
fun finishImageUrl(prefixImageUrl: String?, imageType: ImageType): String {
|
||||
if (prefixImageUrl.isNullOrEmpty()) {
|
||||
return ""
|
||||
}
|
||||
return StringBuilder()
|
||||
.append(prefixImageUrl)
|
||||
.append(imageType.serialName)
|
||||
.toString()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package hu.bbara.purefin.core.data.room.offline
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import hu.bbara.purefin.core.data.room.UuidConverters
|
||||
import hu.bbara.purefin.core.data.room.dao.EpisodeDao
|
||||
import hu.bbara.purefin.core.data.room.dao.MovieDao
|
||||
import hu.bbara.purefin.core.data.room.dao.SeasonDao
|
||||
import hu.bbara.purefin.core.data.room.dao.SeriesDao
|
||||
import hu.bbara.purefin.core.data.room.dao.SmartDownloadDao
|
||||
import hu.bbara.purefin.core.data.room.entity.EpisodeEntity
|
||||
import hu.bbara.purefin.core.data.room.entity.MovieEntity
|
||||
import hu.bbara.purefin.core.data.room.entity.SeasonEntity
|
||||
import hu.bbara.purefin.core.data.room.entity.SeriesEntity
|
||||
import hu.bbara.purefin.core.data.room.entity.SmartDownloadEntity
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
MovieEntity::class,
|
||||
SeriesEntity::class,
|
||||
SeasonEntity::class,
|
||||
EpisodeEntity::class,
|
||||
SmartDownloadEntity::class,
|
||||
],
|
||||
version = 9,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(UuidConverters::class)
|
||||
abstract class OfflineMediaDatabase : RoomDatabase() {
|
||||
abstract fun movieDao(): MovieDao
|
||||
abstract fun seriesDao(): SeriesDao
|
||||
abstract fun seasonDao(): SeasonDao
|
||||
abstract fun episodeDao(): EpisodeDao
|
||||
abstract fun smartDownloadDao(): SmartDownloadDao
|
||||
}
|
||||
@@ -1,54 +1,21 @@
|
||||
package hu.bbara.purefin.core.data.session
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class UserSessionRepository @Inject constructor(
|
||||
private val userSessionDataStore: DataStore<UserSession>
|
||||
) {
|
||||
val session: Flow<UserSession> = userSessionDataStore.data
|
||||
interface UserSessionRepository {
|
||||
val serverUrl: Flow<String>
|
||||
suspend fun setServerUrl(serverUrl: String)
|
||||
|
||||
val serverUrl: Flow<String> = session
|
||||
.map { it.url }
|
||||
val accessToken: Flow<String>
|
||||
suspend fun setAccessToken(accessToken: String)
|
||||
|
||||
suspend fun setServerUrl(serverUrl: String) {
|
||||
userSessionDataStore.updateData {
|
||||
it.copy(url = serverUrl)
|
||||
}
|
||||
}
|
||||
val userId: Flow<UUID?>
|
||||
suspend fun setUserId(userId: UUID?)
|
||||
suspend fun getUserId(): UUID?
|
||||
|
||||
val accessToken: Flow<String> = session
|
||||
.map { it.accessToken }
|
||||
val isLoggedIn: Flow<Boolean>
|
||||
suspend fun setLoggedIn(isLoggedIn: Boolean)
|
||||
|
||||
suspend fun setAccessToken(accessToken: String) {
|
||||
userSessionDataStore.updateData {
|
||||
it.copy(accessToken = accessToken)
|
||||
}
|
||||
}
|
||||
|
||||
val userId: Flow<UUID?> = session
|
||||
.map { it.userId }
|
||||
|
||||
suspend fun setUserId(userId: UUID?) {
|
||||
userSessionDataStore.updateData {
|
||||
it.copy(userId = userId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUserId(): UUID? = userId.first()
|
||||
|
||||
val isLoggedIn: Flow<Boolean> = session.map { it.loggedIn }.distinctUntilChanged()
|
||||
|
||||
suspend fun setLoggedIn(isLoggedIn: Boolean) {
|
||||
userSessionDataStore.updateData {
|
||||
it.copy(loggedIn = isLoggedIn)
|
||||
}
|
||||
}
|
||||
|
||||
val isOfflineMode: Flow<Boolean> = session.map { it.isOfflineMode }.distinctUntilChanged()
|
||||
val isOfflineMode: Flow<Boolean>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "hu.bbara.purefin.feature.download"
|
||||
namespace = "hu.bbara.purefin.core.download"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
@@ -30,11 +30,11 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
implementation(project(":core:data"))
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.hilt)
|
||||
ksp(libs.hilt.compiler)
|
||||
implementation(libs.medi3.exoplayer)
|
||||
implementation(libs.media3.datasource.okhttp)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.jellyfin.core)
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package hu.bbara.purefin.feature.download
|
||||
package hu.bbara.purefin.core.download
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import hu.bbara.purefin.core.data.download.MediaDownloadController
|
||||
import hu.bbara.purefin.core.download.MediaDownloadController
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.feature.download
|
||||
package hu.bbara.purefin.core.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.core.data.download
|
||||
package hu.bbara.purefin.core.download
|
||||
|
||||
sealed class DownloadState {
|
||||
data object NotDownloaded : DownloadState()
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.core.data.download
|
||||
package hu.bbara.purefin.core.download
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.feature.download
|
||||
package hu.bbara.purefin.core.download
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
@@ -9,18 +9,9 @@ import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadRequest
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import hu.bbara.purefin.core.data.DownloadMediaSourceResolver
|
||||
import hu.bbara.purefin.core.data.OfflineCatalogStore
|
||||
import hu.bbara.purefin.core.data.SmartDownloadStore
|
||||
import hu.bbara.purefin.core.data.client.playbackCustomCacheKey
|
||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.core.data.download.DownloadState
|
||||
import hu.bbara.purefin.core.data.download.MediaDownloadController
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.core.model.Season
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -34,10 +25,6 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.PlayMethod
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -47,10 +34,9 @@ import javax.inject.Singleton
|
||||
class MediaDownloadManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val downloadManager: DownloadManager,
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val downloadMediaSourceResolver: DownloadMediaSourceResolver,
|
||||
private val offlineCatalogStore: OfflineCatalogStore,
|
||||
private val smartDownloadStore: SmartDownloadStore,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
) : MediaDownloadController {
|
||||
|
||||
private val stateFlows = ConcurrentHashMap<String, MutableStateFlow<DownloadState>>()
|
||||
@@ -127,53 +113,19 @@ class MediaDownloadManager @Inject constructor(
|
||||
override suspend fun downloadMovie(movieId: UUID) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val sources = jellyfinApiClient.getMediaSources(movieId)
|
||||
val source = sources.firstOrNull() ?: run {
|
||||
Log.e(TAG, "No media sources for $movieId")
|
||||
return@withContext
|
||||
}
|
||||
val shouldTranscode = source.shouldUseTranscodingUrl()
|
||||
|
||||
val url = jellyfinApiClient.getMediaPlaybackUrl(movieId, source) ?: run {
|
||||
Log.e(TAG, "No playback URL for $movieId")
|
||||
val source = downloadMediaSourceResolver.resolveMovieDownload(movieId) ?: run {
|
||||
Log.e(TAG, "No downloadable movie source for $movieId")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val itemInfo = jellyfinApiClient.getItemInfo(movieId) ?: run {
|
||||
Log.e(TAG, "No item info for $movieId")
|
||||
return@withContext
|
||||
}
|
||||
offlineCatalogStore.saveMovies(listOf(source.movie))
|
||||
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||
val movie = Movie(
|
||||
id = itemInfo.id,
|
||||
libraryId = itemInfo.parentId ?: UUID.randomUUID(),
|
||||
title = itemInfo.name ?: "Unknown title",
|
||||
progress = itemInfo.userData?.playedPercentage,
|
||||
watched = itemInfo.userData?.played ?: false,
|
||||
year = itemInfo.productionYear?.toString()
|
||||
?: itemInfo.premiereDate?.year?.toString().orEmpty(),
|
||||
rating = itemInfo.officialRating ?: "NR",
|
||||
runtime = formatRuntime(itemInfo.runTimeTicks),
|
||||
synopsis = itemInfo.overview ?: "No synopsis available",
|
||||
format = itemInfo.container?.uppercase() ?: "VIDEO",
|
||||
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = itemInfo.id
|
||||
),
|
||||
subtitles = "ENG",
|
||||
audioTrack = "ENG",
|
||||
cast = emptyList()
|
||||
)
|
||||
|
||||
offlineCatalogStore.saveMovies(listOf(movie))
|
||||
|
||||
Log.d(TAG, "Starting download for '${movie.title}' from: $url")
|
||||
Log.d(TAG, "Starting download for '${source.movie.title}' from: ${source.playbackUrl}")
|
||||
val request = buildDownloadRequest(
|
||||
mediaId = movieId,
|
||||
playbackUrl = url,
|
||||
title = movie.title,
|
||||
isDirectPlay = !shouldTranscode
|
||||
playbackUrl = source.playbackUrl,
|
||||
title = source.movie.title,
|
||||
customCacheKey = source.customCacheKey,
|
||||
)
|
||||
PurefinDownloadService.sendAddDownload(context, request)
|
||||
Log.d(TAG, "Download request sent for $movieId")
|
||||
@@ -198,50 +150,27 @@ class MediaDownloadManager @Inject constructor(
|
||||
override suspend fun downloadEpisode(episodeId: UUID) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||
val sources = jellyfinApiClient.getMediaSources(episodeId)
|
||||
val source = sources.firstOrNull() ?: run {
|
||||
Log.e(TAG, "No media sources for episode $episodeId")
|
||||
return@withContext
|
||||
}
|
||||
val shouldTranscode = source.shouldUseTranscodingUrl()
|
||||
|
||||
val url = jellyfinApiClient.getMediaPlaybackUrl(episodeId, source) ?: run {
|
||||
Log.e(TAG, "No playback URL for episode $episodeId")
|
||||
val source = downloadMediaSourceResolver.resolveEpisodeDownload(episodeId) ?: run {
|
||||
Log.e(TAG, "No downloadable episode source for $episodeId")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val episode = jellyfinApiClient.getItemInfo(episodeId)?.toEpisode(serverUrl) ?: run {
|
||||
Log.e(TAG, "Episode not found $episodeId")
|
||||
return@withContext
|
||||
if (offlineCatalogStore.getSeriesBasic(source.series.id) == null) {
|
||||
offlineCatalogStore.saveSeries(listOf(source.series))
|
||||
}
|
||||
|
||||
val series = jellyfinApiClient.getItemInfo(episode.seriesId)?.toSeries(serverUrl) ?: run {
|
||||
Log.e(TAG, "Series not found ${episode.seriesId}")
|
||||
return@withContext
|
||||
if (offlineCatalogStore.getSeason(source.season.id) == null) {
|
||||
offlineCatalogStore.saveSeason(source.season)
|
||||
}
|
||||
|
||||
val season = jellyfinApiClient.getItemInfo(episode.seasonId)?.toSeason(series.id) ?: run {
|
||||
Log.e(TAG, "Season not found ${episode.seasonId}")
|
||||
return@withContext
|
||||
}
|
||||
offlineCatalogStore.saveEpisode(source.episode)
|
||||
|
||||
if (offlineCatalogStore.getSeriesBasic(series.id) == null) {
|
||||
offlineCatalogStore.saveSeries(listOf(series))
|
||||
}
|
||||
|
||||
if (offlineCatalogStore.getSeason(season.id) == null) {
|
||||
offlineCatalogStore.saveSeason(season)
|
||||
}
|
||||
|
||||
offlineCatalogStore.saveEpisode(episode)
|
||||
|
||||
Log.d(TAG, "Starting download for episode '${episode.title}' from: $url")
|
||||
Log.d(TAG, "Starting download for episode '${source.episode.title}' from: ${source.playbackUrl}")
|
||||
val request = buildDownloadRequest(
|
||||
mediaId = episodeId,
|
||||
playbackUrl = url,
|
||||
title = episode.title,
|
||||
isDirectPlay = !shouldTranscode
|
||||
playbackUrl = source.playbackUrl,
|
||||
title = source.episode.title,
|
||||
customCacheKey = source.customCacheKey,
|
||||
)
|
||||
PurefinDownloadService.sendAddDownload(context, request)
|
||||
Log.d(TAG, "Download request sent for episode $episodeId")
|
||||
@@ -299,17 +228,13 @@ class MediaDownloadManager @Inject constructor(
|
||||
|
||||
private suspend fun syncSmartDownloadsForSeries(seriesId: UUID) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||
|
||||
// 1. Get currently downloaded episodes for this series
|
||||
val downloadedEpisodes = offlineCatalogStore.getEpisodesBySeries(seriesId)
|
||||
|
||||
// 2. Check watched status from server and delete watched downloads
|
||||
val unwatchedDownloaded = mutableListOf<UUID>()
|
||||
for (episode in downloadedEpisodes) {
|
||||
val itemInfo = jellyfinApiClient.getItemInfo(episode.id)
|
||||
val isWatched = itemInfo?.userData?.played ?: false
|
||||
if (isWatched) {
|
||||
if (downloadMediaSourceResolver.isEpisodeWatched(episode.id)) {
|
||||
Log.d(TAG, "Smart download: removing watched episode ${episode.title}")
|
||||
cancelEpisodeDownload(episode.id)
|
||||
} else {
|
||||
@@ -317,22 +242,15 @@ class MediaDownloadManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Get all episodes of the series from the server in order
|
||||
val seasons = jellyfinApiClient.getSeasons(seriesId)
|
||||
val allEpisodes = mutableListOf<BaseItemDto>()
|
||||
for (season in seasons) {
|
||||
val episodes = jellyfinApiClient.getEpisodesInSeason(seriesId, season.id)
|
||||
allEpisodes.addAll(episodes)
|
||||
}
|
||||
|
||||
// 4. Find unwatched episodes not already downloaded
|
||||
// 3. Find unwatched episodes not already downloaded
|
||||
val needed = SMART_DOWNLOAD_COUNT - unwatchedDownloaded.size
|
||||
if (needed <= 0) return@withContext
|
||||
|
||||
val toDownload = allEpisodes
|
||||
.filter { ep -> ep.userData?.played != true && ep.id !in unwatchedDownloaded }
|
||||
.take(needed)
|
||||
.map { it.id }
|
||||
val toDownload = downloadMediaSourceResolver.getUnwatchedEpisodeIds(
|
||||
seriesId = seriesId,
|
||||
excludedEpisodeIds = unwatchedDownloaded.toSet(),
|
||||
limit = needed,
|
||||
)
|
||||
|
||||
if (toDownload.isNotEmpty()) {
|
||||
Log.d(TAG, "Smart download: queuing ${toDownload.size} episodes for series $seriesId")
|
||||
@@ -349,18 +267,12 @@ class MediaDownloadManager @Inject constructor(
|
||||
mediaId: UUID,
|
||||
playbackUrl: String,
|
||||
title: String,
|
||||
isDirectPlay: Boolean
|
||||
customCacheKey: String?,
|
||||
): DownloadRequest {
|
||||
val builder = DownloadRequest.Builder(mediaId.toString(), playbackUrl.toUri())
|
||||
.setData(title.toByteArray(Charsets.UTF_8))
|
||||
|
||||
if (isDirectPlay) {
|
||||
playbackCustomCacheKey(
|
||||
mediaId = mediaId.toString(),
|
||||
playbackUrl = playbackUrl,
|
||||
playMethod = PlayMethod.DIRECT_PLAY
|
||||
)?.let(builder::setCustomCacheKey)
|
||||
}
|
||||
customCacheKey?.let(builder::setCustomCacheKey)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
@@ -375,73 +287,8 @@ class MediaDownloadManager @Inject constructor(
|
||||
else -> DownloadState.NotDownloaded
|
||||
}
|
||||
|
||||
private fun formatRuntime(ticks: Long?): String {
|
||||
if (ticks == null || ticks <= 0) return "—"
|
||||
val totalSeconds = ticks / 10_000_000
|
||||
val hours = java.util.concurrent.TimeUnit.SECONDS.toHours(totalSeconds)
|
||||
val minutes = java.util.concurrent.TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
|
||||
return if (hours > 0) "${hours}h ${minutes}m" else "${minutes}m"
|
||||
}
|
||||
|
||||
private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
|
||||
return Episode(
|
||||
id = id,
|
||||
seriesId = seriesId!!,
|
||||
seasonId = parentId!!,
|
||||
title = name ?: "Unknown title",
|
||||
index = indexNumber ?: 0,
|
||||
releaseDate = productionYear?.toString() ?: "—",
|
||||
rating = officialRating ?: "NR",
|
||||
runtime = formatRuntime(runTimeTicks),
|
||||
progress = userData?.playedPercentage,
|
||||
watched = userData?.played ?: false,
|
||||
format = container?.uppercase() ?: "VIDEO",
|
||||
synopsis = overview ?: "No synopsis available.",
|
||||
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = id
|
||||
),
|
||||
cast = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun BaseItemDto.toSeries(serverUrl: String): Series {
|
||||
return Series(
|
||||
id = id,
|
||||
libraryId = parentId ?: java.util.UUID.randomUUID(),
|
||||
name = name ?: "Unknown",
|
||||
synopsis = overview ?: "No synopsis available",
|
||||
year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
|
||||
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = id
|
||||
),
|
||||
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
|
||||
seasonCount = childCount ?: 0,
|
||||
seasons = emptyList(),
|
||||
cast = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun BaseItemDto.toSeason(seriesId: UUID): Season {
|
||||
return Season(
|
||||
id = id,
|
||||
seriesId = this.seriesId ?: seriesId,
|
||||
name = name ?: "Unknown",
|
||||
index = indexNumber ?: 0,
|
||||
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
|
||||
episodeCount = childCount ?: 0,
|
||||
episodes = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MediaDownloadManager"
|
||||
private const val SMART_DOWNLOAD_COUNT = 5
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediaSourceInfo.shouldUseTranscodingUrl(): Boolean {
|
||||
return supportsTranscoding == true &&
|
||||
(supportsDirectPlay == false || transcodingUrl != null)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.feature.download
|
||||
package hu.bbara.purefin.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
@@ -50,7 +50,7 @@ class PurefinDownloadService : DownloadService(
|
||||
if (activeDownloads.isEmpty()) {
|
||||
return entryPoint.downloadNotificationHelper().buildProgressNotification(
|
||||
this,
|
||||
R.drawable.ic_launcher_foreground,
|
||||
android.R.drawable.stat_sys_download,
|
||||
null,
|
||||
null,
|
||||
downloads,
|
||||
@@ -95,7 +95,7 @@ class PurefinDownloadService : DownloadService(
|
||||
}
|
||||
|
||||
return NotificationCompat.Builder(this, DOWNLOAD_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentTitle(title)
|
||||
.setContentText(contentText)
|
||||
.setProgress(100, percent.toInt(), false)
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="download_channel_name">Downloads</string>
|
||||
<string name="download_channel_description">Media download progress</string>
|
||||
<string name="download_channel_description">Purefin offline downloads</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.feature.download
|
||||
package hu.bbara.purefin.core.download
|
||||
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.cache.CacheDataSource
|
||||
16
core/image/build.gradle.kts
Normal file
16
core/image/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
jvmToolchain(11)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package hu.bbara.purefin.core.image
|
||||
|
||||
enum class ArtworkKind(
|
||||
internal val pathSegment: String,
|
||||
) {
|
||||
PRIMARY("Primary"),
|
||||
BACKDROP("Backdrop"),
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package hu.bbara.purefin.core.image
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
object ImageUrlBuilder {
|
||||
fun toImageUrl(url: String, itemId: UUID, artworkKind: ArtworkKind): String {
|
||||
if (url.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
return StringBuilder()
|
||||
.append(url)
|
||||
.append("/Items/")
|
||||
.append(itemId)
|
||||
.append("/Images/")
|
||||
.append(artworkKind.pathSegment)
|
||||
.toString()
|
||||
}
|
||||
|
||||
fun toPrefixImageUrl(url: String, itemId: UUID): String {
|
||||
if (url.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
return StringBuilder()
|
||||
.append(url)
|
||||
.append("/Items/")
|
||||
.append(itemId)
|
||||
.append("/Images/")
|
||||
.toString()
|
||||
}
|
||||
|
||||
fun finishImageUrl(prefixImageUrl: String?, artworkKind: ArtworkKind): String {
|
||||
if (prefixImageUrl.isNullOrEmpty()) {
|
||||
return ""
|
||||
}
|
||||
return StringBuilder()
|
||||
.append(prefixImageUrl)
|
||||
.append(artworkKind.pathSegment)
|
||||
.toString()
|
||||
}
|
||||
}
|
||||
38
core/navigation/build.gradle.kts
Normal file
38
core/navigation/build.gradle.kts
Normal file
@@ -0,0 +1,38 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "hu.bbara.purefin.core.navigation"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 29
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.hilt)
|
||||
ksp(libs.hilt.compiler)
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package hu.bbara.purefin.core.navigation
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.Serializable
|
||||
import hu.bbara.purefin.core.navigation.UuidSerializer
|
||||
|
||||
@Serializable
|
||||
data class EpisodeDto(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val id: UUID,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val seasonId: UUID,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val seriesId: UUID,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package hu.bbara.purefin.core.navigation
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.Serializable
|
||||
import hu.bbara.purefin.core.navigation.UuidSerializer
|
||||
|
||||
@Serializable
|
||||
data class LibraryDto(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package hu.bbara.purefin.core.navigation
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.Serializable
|
||||
import hu.bbara.purefin.core.navigation.UuidSerializer
|
||||
|
||||
@Serializable
|
||||
data class MovieDto(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val id: UUID,
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
package hu.bbara.purefin.core.navigation
|
||||
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
package hu.bbara.purefin.core.navigation
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
package hu.bbara.purefin.core.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -0,0 +1,11 @@
|
||||
package hu.bbara.purefin.core.navigation
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.Serializable
|
||||
import hu.bbara.purefin.core.navigation.UuidSerializer
|
||||
|
||||
@Serializable
|
||||
data class SeriesDto(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val id: UUID,
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
package hu.bbara.purefin.core.navigation
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
object UuidSerializer : KSerializer<UUID> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
|
||||
|
||||
override fun serialize(encoder: Encoder, value: UUID) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.feature.shared.navigation
|
||||
package hu.bbara.purefin.core.navigation
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.decodeFromString
|
||||
@@ -31,6 +31,7 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
implementation(project(":core:data"))
|
||||
implementation(project(":core:image"))
|
||||
implementation(libs.hilt)
|
||||
ksp(libs.hilt.compiler)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import hu.bbara.purefin.core.data.client.PlaybackReportContext
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
import hu.bbara.purefin.core.player.model.QueueItemUi
|
||||
import hu.bbara.purefin.core.player.model.TrackOption
|
||||
import hu.bbara.purefin.core.player.model.TrackType
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.util.Log
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import hu.bbara.purefin.core.data.MediaProgressWriter
|
||||
import hu.bbara.purefin.core.data.PlaybackProgressReporter
|
||||
import hu.bbara.purefin.core.data.client.PlaybackReportContext
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
41
data/catalog/build.gradle.kts
Normal file
41
data/catalog/build.gradle.kts
Normal file
@@ -0,0 +1,41 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "hu.bbara.purefin.data.catalog"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 29
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
implementation(project(":core:data"))
|
||||
implementation(project(":core:image"))
|
||||
implementation(project(":data:jellyfin"))
|
||||
implementation(project(":data:offline"))
|
||||
implementation(libs.jellyfin.core)
|
||||
implementation(libs.hilt)
|
||||
ksp(libs.hilt.compiler)
|
||||
implementation(libs.datastore)
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
package hu.bbara.purefin.data.catalog
|
||||
|
||||
import hu.bbara.purefin.core.data.MediaCatalogReader
|
||||
import hu.bbara.purefin.core.data.MediaProgressWriter
|
||||
import hu.bbara.purefin.core.data.NetworkMonitor
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user