mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
feat: introduce core data models and interfaces for playback and downloads
This commit is contained in:
@@ -52,9 +52,19 @@ kotlin {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":core:model"))
|
implementation(project(":core:model"))
|
||||||
|
implementation(project(":core:image"))
|
||||||
implementation(project(":core:data"))
|
implementation(project(":core:data"))
|
||||||
|
implementation(project(":core:navigation"))
|
||||||
|
implementation(project(":core:download"))
|
||||||
implementation(project(":core:player"))
|
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.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
@@ -83,8 +93,6 @@ dependencies {
|
|||||||
implementation(libs.media3.datasource.okhttp)
|
implementation(libs.media3.datasource.okhttp)
|
||||||
implementation(libs.androidx.navigation3.runtime)
|
implementation(libs.androidx.navigation3.runtime)
|
||||||
implementation(libs.androidx.navigation3.ui)
|
implementation(libs.androidx.navigation3.ui)
|
||||||
implementation(libs.androidx.room.ktx)
|
|
||||||
ksp(libs.androidx.room.compiler)
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import androidx.compose.ui.input.key.Key
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
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.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.tv.home.TvHomeScreen
|
import hu.bbara.purefin.tv.home.TvHomeScreen
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
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.junit4.createAndroidComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.performClick
|
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 hu.bbara.purefin.ui.theme.AppTheme
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import org.junit.Assert.assertEquals
|
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.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.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
import hu.bbara.purefin.core.navigation.EpisodeDto
|
||||||
import hu.bbara.purefin.tv.navigation.LocalNavigationManager
|
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.core.model.Episode
|
||||||
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
import hu.bbara.purefin.feature.content.episode.EpisodeScreenViewModel
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvEpisodeScreen(
|
fun TvEpisodeScreen(
|
||||||
@@ -87,7 +87,7 @@ internal fun TvEpisodeScreenContent(
|
|||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
TvMediaDetailBodyBox(
|
TvMediaDetailBodyBox(
|
||||||
backgroundImageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
backgroundImageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
modifier = it
|
modifier = it
|
||||||
) {
|
) {
|
||||||
TvEpisodeHeroSection(
|
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.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.feature.shared.navigation.MovieDto
|
import hu.bbara.purefin.core.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.content.movie.MovieScreenViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvMovieScreen(
|
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.MediaResumeButton
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
|
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.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
|
||||||
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.content.series.SeriesViewModel
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
internal const val SeriesPlayButtonTag = "series-play-button"
|
internal const val SeriesPlayButtonTag = "series-play-button"
|
||||||
internal const val SeriesFirstSeasonTabTag = "series-first-season-tab"
|
internal const val SeriesFirstSeasonTabTag = "series-first-season-tab"
|
||||||
@@ -296,7 +296,7 @@ private fun TvEpisodeCard(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
model = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
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.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.feature.shared.navigation.SeriesDto
|
import hu.bbara.purefin.core.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.content.series.SeriesViewModel
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@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.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator
|
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.browse.home.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.common.ui.MediaSynopsis
|
import hu.bbara.purefin.common.ui.MediaSynopsis
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
internal val MediaDetailHorizontalPadding = 48.dp
|
internal val MediaDetailHorizontalPadding = 48.dp
|
||||||
private val MediaDetailHeaderTopPadding = 104.dp
|
private val MediaDetailHeaderTopPadding = 104.dp
|
||||||
@@ -151,8 +151,8 @@ internal fun TvMediaDetailBodyBox(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun tvMediaDetailBackgroundImageUrl(imageUrlPrefix: String?): String {
|
internal fun tvMediaDetailBackgroundImageUrl(imageUrlPrefix: String?): String {
|
||||||
val primaryImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
val primaryImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||||
val backdropImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.BACKDROP)
|
val backdropImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.BACKDROP)
|
||||||
return backdropImageUrl.ifBlank { primaryImageUrl }
|
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.PurefinPasswordField
|
||||||
import hu.bbara.purefin.common.ui.PurefinTextButton
|
import hu.bbara.purefin.common.ui.PurefinTextButton
|
||||||
import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
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
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ 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.common.ui.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
import hu.bbara.purefin.core.data.SessionBootstrapper
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
|
import hu.bbara.purefin.data.jellyfin.client.JellyfinAuthInterceptor
|
||||||
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.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.LocalNavigationBackStack
|
||||||
import hu.bbara.purefin.tv.navigation.LocalNavigationManager
|
import hu.bbara.purefin.tv.navigation.LocalNavigationManager
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
@@ -66,7 +66,7 @@ class TvActivity : ComponentActivity() {
|
|||||||
lateinit var navigationManager: NavigationManager
|
lateinit var navigationManager: NavigationManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var jellyfinApiClient: JellyfinApiClient
|
lateinit var sessionBootstrapper: SessionBootstrapper
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var authInterceptor: JellyfinAuthInterceptor
|
lateinit var authInterceptor: JellyfinAuthInterceptor
|
||||||
@@ -94,7 +94,7 @@ class TvActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun init() {
|
private suspend fun init() {
|
||||||
jellyfinApiClient.updateApiClient()
|
sessionBootstrapper.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureImageLoader() {
|
private fun configureImageLoader() {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ 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.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.TvHomeScreen
|
||||||
import hu.bbara.purefin.tv.home.ui.TvDrawerDestination
|
import hu.bbara.purefin.tv.home.ui.TvDrawerDestination
|
||||||
import hu.bbara.purefin.tv.home.ui.TvDrawerDestinationItem
|
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.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import hu.bbara.purefin.core.data.client.PlaybackProfileFamily
|
import hu.bbara.purefin.core.data.PlaybackProfileFamily
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
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.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.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
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package hu.bbara.purefin.tv.home.ui
|
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.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 hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
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
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
internal data class TvFocusedHeroModel(
|
internal data class TvFocusedHeroModel(
|
||||||
@@ -42,7 +42,7 @@ internal fun FocusableItem.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun backdropImageUrl(imageUrlPrefix: String?, fallbackImageUrl: String): String {
|
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 }
|
return backdropImageUrl.ifBlank { fallbackImageUrl }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ private fun Movie.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
|||||||
id = id,
|
id = id,
|
||||||
backdropImageUrl = backdropImageUrl(
|
backdropImageUrl = backdropImageUrl(
|
||||||
imageUrlPrefix = imageUrlPrefix,
|
imageUrlPrefix = imageUrlPrefix,
|
||||||
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||||
),
|
),
|
||||||
title = title,
|
title = title,
|
||||||
metadata = listOf(year, rating, runtime, format).compactMetadata(),
|
metadata = listOf(year, rating, runtime, format).compactMetadata(),
|
||||||
@@ -63,7 +63,7 @@ private fun Episode.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
|||||||
id = id,
|
id = id,
|
||||||
backdropImageUrl = backdropImageUrl(
|
backdropImageUrl = backdropImageUrl(
|
||||||
imageUrlPrefix = imageUrlPrefix,
|
imageUrlPrefix = imageUrlPrefix,
|
||||||
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||||
),
|
),
|
||||||
title = title,
|
title = title,
|
||||||
metadata = listOf(
|
metadata = listOf(
|
||||||
@@ -87,7 +87,7 @@ private fun Series.toTvFocusedHeroModel(): TvFocusedHeroModel {
|
|||||||
id = id,
|
id = id,
|
||||||
backdropImageUrl = backdropImageUrl(
|
backdropImageUrl = backdropImageUrl(
|
||||||
imageUrlPrefix = imageUrlPrefix,
|
imageUrlPrefix = imageUrlPrefix,
|
||||||
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
|
fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
|
||||||
),
|
),
|
||||||
title = name,
|
title = name,
|
||||||
metadata = listOf(year, seasonLabel(seasonCount), unwatchedText).compactMetadata(),
|
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.LocalDensity
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"
|
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.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
internal data class TvHomeItemRegistry(
|
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.PosterCard
|
||||||
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
import hu.bbara.purefin.common.ui.components.MediaProgressBar
|
||||||
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.FocusableItem
|
import hu.bbara.purefin.feature.browse.home.FocusableItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
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 TvHomeSectionsThumbShape = RoundedCornerShape(20.dp)
|
||||||
private val TvHomeSectionsHorizontalPadding = 32.dp
|
private val TvHomeSectionsHorizontalPadding = 32.dp
|
||||||
@@ -93,14 +93,14 @@ fun TvContinueWatchingSection(
|
|||||||
title = item.primaryText,
|
title = item.primaryText,
|
||||||
supporting = item.secondaryText,
|
supporting = item.secondaryText,
|
||||||
imageUrl = when (item.type) {
|
imageUrl = when (item.type) {
|
||||||
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl(
|
||||||
prefixImageUrl = item.movie?.imageUrlPrefix,
|
prefixImageUrl = item.movie?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
artworkKind = ArtworkKind.PRIMARY
|
||||||
)
|
)
|
||||||
|
|
||||||
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl(
|
||||||
prefixImageUrl = item.episode?.imageUrlPrefix,
|
prefixImageUrl = item.episode?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
artworkKind = ArtworkKind.PRIMARY
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ 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.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
|
import hu.bbara.purefin.core.model.LibraryKind
|
||||||
|
|
||||||
internal const val TvLibrariesOverviewItemTagPrefix = "tv-libraries-overview-item-"
|
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 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.feature.shared.navigation.LibraryDto
|
import hu.bbara.purefin.core.navigation.LibraryDto
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
import hu.bbara.purefin.feature.browse.library.LibraryViewModel
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package hu.bbara.purefin.tv.navigation
|
|||||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
|
|
||||||
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
||||||
staticCompositionLocalOf { error("NavigationManager not provided") }
|
staticCompositionLocalOf { error("NavigationManager not provided") }
|
||||||
|
|||||||
@@ -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.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(ActivityRetainedComponent::class)
|
@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.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.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.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
|
||||||
|
|||||||
@@ -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
|
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.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 hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
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.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -33,7 +33,7 @@ class TvFocusedItemHeroTest {
|
|||||||
assertEquals("42%", hero.progressLabel)
|
assertEquals("42%", hero.progressLabel)
|
||||||
assertNull(hero.watchedText)
|
assertNull(hero.watchedText)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.BACKDROP),
|
ImageUrlBuilder.finishImageUrl(movie.imageUrlPrefix, ArtworkKind.BACKDROP),
|
||||||
hero.backdropImageUrl
|
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.Episode
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
|||||||
@@ -51,10 +51,19 @@ kotlin {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":core:model"))
|
implementation(project(":core:model"))
|
||||||
|
implementation(project(":core:image"))
|
||||||
implementation(project(":core:data"))
|
implementation(project(":core:data"))
|
||||||
implementation(project(":feature:download"))
|
implementation(project(":core:navigation"))
|
||||||
|
implementation(project(":core:download"))
|
||||||
implementation(project(":core:player"))
|
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.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
@@ -83,8 +92,6 @@ dependencies {
|
|||||||
implementation(libs.media3.datasource.okhttp)
|
implementation(libs.media3.datasource.okhttp)
|
||||||
implementation(libs.androidx.navigation3.runtime)
|
implementation(libs.androidx.navigation3.runtime)
|
||||||
implementation(libs.androidx.navigation3.ui)
|
implementation(libs.androidx.navigation3.ui)
|
||||||
implementation(libs.androidx.room.ktx)
|
|
||||||
ksp(libs.androidx.room.compiler)
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Theme.Purefin" />
|
android:theme="@style/Theme.Purefin" />
|
||||||
<service
|
<service
|
||||||
android:name="hu.bbara.purefin.feature.download.PurefinDownloadService"
|
android:name="hu.bbara.purefin.core.download.PurefinDownloadService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -36,15 +36,15 @@ 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.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.core.data.SessionBootstrapper
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
import hu.bbara.purefin.data.jellyfin.client.JellyfinAuthInterceptor
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
|
|
||||||
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.navigation.LocalNavigationBackStack
|
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
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.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
|
||||||
@@ -65,7 +65,7 @@ class PurefinActivity : ComponentActivity() {
|
|||||||
lateinit var navigationManager: NavigationManager
|
lateinit var navigationManager: NavigationManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var jellyfinApiClient: JellyfinApiClient
|
lateinit var sessionBootstrapper: SessionBootstrapper
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var authInterceptor: JellyfinAuthInterceptor
|
lateinit var authInterceptor: JellyfinAuthInterceptor
|
||||||
@@ -93,7 +93,7 @@ class PurefinActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun init() {
|
private suspend fun init() {
|
||||||
jellyfinApiClient.updateApiClient()
|
sessionBootstrapper.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureImageLoader() {
|
private fun configureImageLoader() {
|
||||||
|
|||||||
@@ -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.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
import hu.bbara.purefin.navigation.appRouteEntryBuilder
|
import hu.bbara.purefin.navigation.appRouteEntryBuilder
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import dagger.Module
|
|||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import hu.bbara.purefin.core.data.client.PlaybackProfileFamily
|
import hu.bbara.purefin.core.data.PlaybackProfileFamily
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package hu.bbara.purefin.navigation
|
|||||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
|
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||||
import hu.bbara.purefin.feature.shared.navigation.Route
|
import hu.bbara.purefin.core.navigation.Route
|
||||||
|
|
||||||
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
||||||
staticCompositionLocalOf { error("NavigationManager not provided") }
|
staticCompositionLocalOf { error("NavigationManager not provided") }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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.core.navigation.Route
|
||||||
import hu.bbara.purefin.ui.screen.library.LibraryScreen
|
import hu.bbara.purefin.ui.screen.library.LibraryScreen
|
||||||
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
||||||
import hu.bbara.purefin.ui.screen.AppScreen
|
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.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.feature.shared.search.SearchViewModel
|
import hu.bbara.purefin.feature.search.SearchViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultTopBar(
|
fun DefaultTopBar(
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ 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.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.UnwatchedEpisodeIndicator
|
||||||
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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.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.download.DownloadsScreen
|
||||||
import hu.bbara.purefin.ui.screen.home.HomeScreen
|
import hu.bbara.purefin.ui.screen.home.HomeScreen
|
||||||
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.feature.shared.download.ActiveDownloadItem
|
import hu.bbara.purefin.feature.downloads.ActiveDownloadItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DownloadingItemRow(
|
internal fun DownloadingItemRow(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ 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.ui.common.card.PosterCard
|
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||||
import hu.bbara.purefin.feature.shared.download.DownloadsViewModel
|
import hu.bbara.purefin.feature.downloads.DownloadsViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadsContent(
|
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.common.media.MediaMetaChip
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.download.DownloadState
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
import hu.bbara.purefin.core.navigation.EpisodeDto
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
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.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.content.episode.EpisodeScreenViewModel
|
||||||
import hu.bbara.purefin.ui.screen.episode.components.EpisodeDetails
|
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.EpisodeTopBar
|
||||||
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBarShortcut
|
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBarShortcut
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -167,7 +167,7 @@ private fun EpisodeHeroSection(
|
|||||||
.height(sectionHeight)
|
.height(sectionHeight)
|
||||||
) {
|
) {
|
||||||
MediaHero(
|
MediaHero(
|
||||||
imageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
imageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
backgroundColor = scheme.background,
|
backgroundColor = scheme.background,
|
||||||
modifier = Modifier.fillMaxSize()
|
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.button.MediaActionButton
|
||||||
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
||||||
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
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.core.model.Episode
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
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.HomeContent
|
||||||
import hu.bbara.purefin.ui.screen.home.components.HomeTopBar
|
import hu.bbara.purefin.ui.screen.home.components.HomeTopBar
|
||||||
import hu.bbara.purefin.ui.screen.home.components.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.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
import hu.bbara.purefin.feature.browse.home.SuggestedItem
|
||||||
import hu.bbara.purefin.ui.screen.AppBottomBar
|
import hu.bbara.purefin.ui.screen.AppBottomBar
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ 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.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import hu.bbara.purefin.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
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.continuewatching.ContinueWatchingSection
|
||||||
import hu.bbara.purefin.ui.screen.home.components.featured.SuggestionsSection
|
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.library.LibraryPosterSection
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
|
import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
import hu.bbara.purefin.core.model.MediaKind
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ContinueWatchingCard(
|
internal fun ContinueWatchingCard(
|
||||||
@@ -53,13 +53,13 @@ internal fun ContinueWatchingCard(
|
|||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
val imageUrl = when (item.type) {
|
val imageUrl = when (item.type) {
|
||||||
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl(
|
||||||
prefixImageUrl = item.movie?.imageUrlPrefix,
|
prefixImageUrl = item.movie?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
artworkKind = ArtworkKind.PRIMARY
|
||||||
)
|
)
|
||||||
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl(
|
MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl(
|
||||||
prefixImageUrl = item.episode?.imageUrlPrefix,
|
prefixImageUrl = item.episode?.imageUrlPrefix,
|
||||||
imageType = ImageType.PRIMARY
|
artworkKind = ArtworkKind.PRIMARY
|
||||||
)
|
)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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.ui.common.header.SectionHeader
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
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
|
@Composable
|
||||||
internal fun SuggestionCard(
|
internal fun SuggestionCard(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.feature.shared.home.SuggestedItem
|
import hu.bbara.purefin.feature.browse.home.SuggestedItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SuggestionsSection(
|
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.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator
|
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator
|
||||||
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
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 java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
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.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.feature.shared.home.LibraryItem
|
import hu.bbara.purefin.feature.browse.home.LibraryItem
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.ui.common.header.SectionHeader
|
import hu.bbara.purefin.ui.common.header.SectionHeader
|
||||||
import java.util.UUID
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.feature.shared.home.NextUpItem
|
import hu.bbara.purefin.feature.browse.home.NextUpItem
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun NextUpCard(
|
internal fun NextUpCard(
|
||||||
@@ -57,7 +57,7 @@ internal fun NextUpCard(
|
|||||||
.background(scheme.surface)
|
.background(scheme.surface)
|
||||||
) {
|
) {
|
||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = JellyfinImageHelper.finishImageUrl(item.episode.imageUrlPrefix, ImageType.PRIMARY),
|
model = ImageUrlBuilder.finishImageUrl(item.episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
contentDescription = item.primaryText,
|
contentDescription = item.primaryText,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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.ui.common.header.SectionHeader
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.Dp
|
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.search.SearchViewModel
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import hu.bbara.purefin.core.model.MediaKind
|
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
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import hu.bbara.purefin.ui.common.card.PosterCard
|
import hu.bbara.purefin.ui.common.card.PosterCard
|
||||||
import hu.bbara.purefin.ui.common.button.PurefinIconButton
|
import hu.bbara.purefin.ui.common.button.PurefinIconButton
|
||||||
import hu.bbara.purefin.feature.shared.navigation.LibraryDto
|
import hu.bbara.purefin.core.navigation.LibraryDto
|
||||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
import hu.bbara.purefin.feature.browse.home.PosterItem
|
||||||
import hu.bbara.purefin.feature.shared.library.LibraryViewModel
|
import hu.bbara.purefin.feature.browse.library.LibraryViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryScreen(
|
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.textfield.PurefinPasswordField
|
||||||
import hu.bbara.purefin.ui.common.button.PurefinTextButton
|
import hu.bbara.purefin.ui.common.button.PurefinTextButton
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
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
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -35,16 +35,16 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import hu.bbara.purefin.ui.common.media.MediaMetaChip
|
import hu.bbara.purefin.ui.common.media.MediaMetaChip
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.download.DownloadState
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.feature.shared.navigation.MovieDto
|
import hu.bbara.purefin.core.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.content.movie.MovieScreenViewModel
|
||||||
import hu.bbara.purefin.ui.screen.movie.components.MovieDetails
|
import hu.bbara.purefin.ui.screen.movie.components.MovieDetails
|
||||||
import hu.bbara.purefin.ui.screen.movie.components.MovieTopBar
|
import hu.bbara.purefin.ui.screen.movie.components.MovieTopBar
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -139,7 +139,7 @@ fun MediaHeroSection(
|
|||||||
.height(sectionHeight)
|
.height(sectionHeight)
|
||||||
) {
|
) {
|
||||||
MediaHero(
|
MediaHero(
|
||||||
imageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY),
|
imageUrl = ImageUrlBuilder.finishImageUrl(movie.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
backgroundColor = MaterialTheme.colorScheme.background,
|
backgroundColor = MaterialTheme.colorScheme.background,
|
||||||
modifier = Modifier.fillMaxSize()
|
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.button.MediaActionButton
|
||||||
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
|
||||||
import hu.bbara.purefin.ui.common.button.MediaResumeButton
|
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.core.model.Movie
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
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.common.media.MediaSynopsis
|
||||||
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
|
||||||
import hu.bbara.purefin.ui.common.media.MediaHero
|
import hu.bbara.purefin.ui.common.media.MediaHero
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.download.DownloadState
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.feature.shared.navigation.SeriesDto
|
import hu.bbara.purefin.core.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
|
||||||
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.content.series.SeriesViewModel
|
||||||
import hu.bbara.purefin.ui.screen.series.components.CastRow
|
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.EpisodeCarousel
|
||||||
import hu.bbara.purefin.ui.screen.series.components.SeasonTabs
|
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.SeriesMetaChips
|
||||||
import hu.bbara.purefin.ui.screen.series.components.SeriesTopBar
|
import hu.bbara.purefin.ui.screen.series.components.SeriesTopBar
|
||||||
import hu.bbara.purefin.ui.theme.AppTheme
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -207,7 +207,7 @@ private fun SeriesHeroSection(
|
|||||||
.height(sectionHeight)
|
.height(sectionHeight)
|
||||||
) {
|
) {
|
||||||
MediaHero(
|
MediaHero(
|
||||||
imageUrl = JellyfinImageHelper.finishImageUrl(series.imageUrlPrefix, ImageType.PRIMARY),
|
imageUrl = ImageUrlBuilder.finishImageUrl(series.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
backgroundColor = scheme.background,
|
backgroundColor = scheme.background,
|
||||||
modifier = Modifier.fillMaxSize()
|
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.button.MediaResumeButton
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
|
||||||
import hu.bbara.purefin.core.data.download.DownloadState
|
import hu.bbara.purefin.core.download.DownloadState
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||||
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto
|
import hu.bbara.purefin.core.navigation.EpisodeDto
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
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.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
|
||||||
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.content.series.SeriesViewModel
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import hu.bbara.purefin.core.image.ArtworkKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SeriesTopBar(
|
internal fun SeriesTopBar(
|
||||||
@@ -349,7 +349,7 @@ private fun EpisodeCard(
|
|||||||
.border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp))
|
.border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp))
|
||||||
) {
|
) {
|
||||||
PurefinAsyncImage(
|
PurefinAsyncImage(
|
||||||
model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY),
|
model = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
|
|||||||
@@ -3,9 +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.serialization)
|
|
||||||
alias(libs.plugins.hilt)
|
|
||||||
alias(libs.plugins.ksp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -30,15 +27,7 @@ kotlin {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":core:model"))
|
implementation(project(":core:model"))
|
||||||
implementation(libs.jellyfin.core)
|
|
||||||
implementation(libs.media3.common)
|
implementation(libs.media3.common)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
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)
|
|
||||||
testImplementation(libs.junit)
|
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
|
package hu.bbara.purefin.core.data
|
||||||
|
|
||||||
import hu.bbara.purefin.core.data.client.PlaybackReportContext
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
interface PlaybackProgressReporter {
|
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
|
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 java.util.UUID
|
||||||
import javax.inject.Inject
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class UserSessionRepository @Inject constructor(
|
interface UserSessionRepository {
|
||||||
private val userSessionDataStore: DataStore<UserSession>
|
val serverUrl: Flow<String>
|
||||||
) {
|
suspend fun setServerUrl(serverUrl: String)
|
||||||
val session: Flow<UserSession> = userSessionDataStore.data
|
|
||||||
|
|
||||||
val serverUrl: Flow<String> = session
|
val accessToken: Flow<String>
|
||||||
.map { it.url }
|
suspend fun setAccessToken(accessToken: String)
|
||||||
|
|
||||||
suspend fun setServerUrl(serverUrl: String) {
|
val userId: Flow<UUID?>
|
||||||
userSessionDataStore.updateData {
|
suspend fun setUserId(userId: UUID?)
|
||||||
it.copy(url = serverUrl)
|
suspend fun getUserId(): UUID?
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val accessToken: Flow<String> = session
|
val isLoggedIn: Flow<Boolean>
|
||||||
.map { it.accessToken }
|
suspend fun setLoggedIn(isLoggedIn: Boolean)
|
||||||
|
|
||||||
suspend fun setAccessToken(accessToken: String) {
|
val isOfflineMode: Flow<Boolean>
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "hu.bbara.purefin.feature.download"
|
namespace = "hu.bbara.purefin.core.download"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -30,11 +30,11 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":core:model"))
|
implementation(project(":core:model"))
|
||||||
implementation(project(":core:data"))
|
implementation(project(":core:data"))
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.hilt)
|
implementation(libs.hilt)
|
||||||
ksp(libs.hilt.compiler)
|
ksp(libs.hilt.compiler)
|
||||||
implementation(libs.medi3.exoplayer)
|
implementation(libs.medi3.exoplayer)
|
||||||
implementation(libs.media3.datasource.okhttp)
|
implementation(libs.media3.datasource.okhttp)
|
||||||
implementation(libs.okhttp)
|
implementation(libs.okhttp)
|
||||||
implementation(libs.jellyfin.core)
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package hu.bbara.purefin.feature.download
|
package hu.bbara.purefin.core.download
|
||||||
|
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import hu.bbara.purefin.core.data.download.MediaDownloadController
|
import hu.bbara.purefin.core.download.MediaDownloadController
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.feature.download
|
package hu.bbara.purefin.core.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.core.data.download
|
package hu.bbara.purefin.core.download
|
||||||
|
|
||||||
sealed class DownloadState {
|
sealed class DownloadState {
|
||||||
data object NotDownloaded : 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.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.content.Context
|
||||||
import android.util.Log
|
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.DownloadManager
|
||||||
import androidx.media3.exoplayer.offline.DownloadRequest
|
import androidx.media3.exoplayer.offline.DownloadRequest
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
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.OfflineCatalogStore
|
||||||
import hu.bbara.purefin.core.data.SmartDownloadStore
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -34,10 +25,6 @@ import kotlinx.coroutines.flow.flowOn
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
|
||||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
|
||||||
import org.jellyfin.sdk.model.api.PlayMethod
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -47,10 +34,9 @@ import javax.inject.Singleton
|
|||||||
class MediaDownloadManager @Inject constructor(
|
class MediaDownloadManager @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val downloadManager: DownloadManager,
|
private val downloadManager: DownloadManager,
|
||||||
private val jellyfinApiClient: JellyfinApiClient,
|
private val downloadMediaSourceResolver: DownloadMediaSourceResolver,
|
||||||
private val offlineCatalogStore: OfflineCatalogStore,
|
private val offlineCatalogStore: OfflineCatalogStore,
|
||||||
private val smartDownloadStore: SmartDownloadStore,
|
private val smartDownloadStore: SmartDownloadStore,
|
||||||
private val userSessionRepository: UserSessionRepository,
|
|
||||||
) : MediaDownloadController {
|
) : MediaDownloadController {
|
||||||
|
|
||||||
private val stateFlows = ConcurrentHashMap<String, MutableStateFlow<DownloadState>>()
|
private val stateFlows = ConcurrentHashMap<String, MutableStateFlow<DownloadState>>()
|
||||||
@@ -127,53 +113,19 @@ class MediaDownloadManager @Inject constructor(
|
|||||||
override suspend fun downloadMovie(movieId: UUID) {
|
override suspend fun downloadMovie(movieId: UUID) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val sources = jellyfinApiClient.getMediaSources(movieId)
|
val source = downloadMediaSourceResolver.resolveMovieDownload(movieId) ?: run {
|
||||||
val source = sources.firstOrNull() ?: run {
|
Log.e(TAG, "No downloadable movie source for $movieId")
|
||||||
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")
|
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
|
|
||||||
val itemInfo = jellyfinApiClient.getItemInfo(movieId) ?: run {
|
offlineCatalogStore.saveMovies(listOf(source.movie))
|
||||||
Log.e(TAG, "No item info for $movieId")
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
|
|
||||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
Log.d(TAG, "Starting download for '${source.movie.title}' from: ${source.playbackUrl}")
|
||||||
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")
|
|
||||||
val request = buildDownloadRequest(
|
val request = buildDownloadRequest(
|
||||||
mediaId = movieId,
|
mediaId = movieId,
|
||||||
playbackUrl = url,
|
playbackUrl = source.playbackUrl,
|
||||||
title = movie.title,
|
title = source.movie.title,
|
||||||
isDirectPlay = !shouldTranscode
|
customCacheKey = source.customCacheKey,
|
||||||
)
|
)
|
||||||
PurefinDownloadService.sendAddDownload(context, request)
|
PurefinDownloadService.sendAddDownload(context, request)
|
||||||
Log.d(TAG, "Download request sent for $movieId")
|
Log.d(TAG, "Download request sent for $movieId")
|
||||||
@@ -198,50 +150,27 @@ class MediaDownloadManager @Inject constructor(
|
|||||||
override suspend fun downloadEpisode(episodeId: UUID) {
|
override suspend fun downloadEpisode(episodeId: UUID) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
val source = downloadMediaSourceResolver.resolveEpisodeDownload(episodeId) ?: run {
|
||||||
val sources = jellyfinApiClient.getMediaSources(episodeId)
|
Log.e(TAG, "No downloadable episode source for $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")
|
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
|
|
||||||
val episode = jellyfinApiClient.getItemInfo(episodeId)?.toEpisode(serverUrl) ?: run {
|
if (offlineCatalogStore.getSeriesBasic(source.series.id) == null) {
|
||||||
Log.e(TAG, "Episode not found $episodeId")
|
offlineCatalogStore.saveSeries(listOf(source.series))
|
||||||
return@withContext
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val series = jellyfinApiClient.getItemInfo(episode.seriesId)?.toSeries(serverUrl) ?: run {
|
if (offlineCatalogStore.getSeason(source.season.id) == null) {
|
||||||
Log.e(TAG, "Series not found ${episode.seriesId}")
|
offlineCatalogStore.saveSeason(source.season)
|
||||||
return@withContext
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val season = jellyfinApiClient.getItemInfo(episode.seasonId)?.toSeason(series.id) ?: run {
|
offlineCatalogStore.saveEpisode(source.episode)
|
||||||
Log.e(TAG, "Season not found ${episode.seasonId}")
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offlineCatalogStore.getSeriesBasic(series.id) == null) {
|
Log.d(TAG, "Starting download for episode '${source.episode.title}' from: ${source.playbackUrl}")
|
||||||
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")
|
|
||||||
val request = buildDownloadRequest(
|
val request = buildDownloadRequest(
|
||||||
mediaId = episodeId,
|
mediaId = episodeId,
|
||||||
playbackUrl = url,
|
playbackUrl = source.playbackUrl,
|
||||||
title = episode.title,
|
title = source.episode.title,
|
||||||
isDirectPlay = !shouldTranscode
|
customCacheKey = source.customCacheKey,
|
||||||
)
|
)
|
||||||
PurefinDownloadService.sendAddDownload(context, request)
|
PurefinDownloadService.sendAddDownload(context, request)
|
||||||
Log.d(TAG, "Download request sent for episode $episodeId")
|
Log.d(TAG, "Download request sent for episode $episodeId")
|
||||||
@@ -299,17 +228,13 @@ class MediaDownloadManager @Inject constructor(
|
|||||||
|
|
||||||
private suspend fun syncSmartDownloadsForSeries(seriesId: UUID) {
|
private suspend fun syncSmartDownloadsForSeries(seriesId: UUID) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
|
||||||
|
|
||||||
// 1. Get currently downloaded episodes for this series
|
// 1. Get currently downloaded episodes for this series
|
||||||
val downloadedEpisodes = offlineCatalogStore.getEpisodesBySeries(seriesId)
|
val downloadedEpisodes = offlineCatalogStore.getEpisodesBySeries(seriesId)
|
||||||
|
|
||||||
// 2. Check watched status from server and delete watched downloads
|
// 2. Check watched status from server and delete watched downloads
|
||||||
val unwatchedDownloaded = mutableListOf<UUID>()
|
val unwatchedDownloaded = mutableListOf<UUID>()
|
||||||
for (episode in downloadedEpisodes) {
|
for (episode in downloadedEpisodes) {
|
||||||
val itemInfo = jellyfinApiClient.getItemInfo(episode.id)
|
if (downloadMediaSourceResolver.isEpisodeWatched(episode.id)) {
|
||||||
val isWatched = itemInfo?.userData?.played ?: false
|
|
||||||
if (isWatched) {
|
|
||||||
Log.d(TAG, "Smart download: removing watched episode ${episode.title}")
|
Log.d(TAG, "Smart download: removing watched episode ${episode.title}")
|
||||||
cancelEpisodeDownload(episode.id)
|
cancelEpisodeDownload(episode.id)
|
||||||
} else {
|
} else {
|
||||||
@@ -317,22 +242,15 @@ class MediaDownloadManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Get all episodes of the series from the server in order
|
// 3. Find unwatched episodes not already downloaded
|
||||||
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
|
|
||||||
val needed = SMART_DOWNLOAD_COUNT - unwatchedDownloaded.size
|
val needed = SMART_DOWNLOAD_COUNT - unwatchedDownloaded.size
|
||||||
if (needed <= 0) return@withContext
|
if (needed <= 0) return@withContext
|
||||||
|
|
||||||
val toDownload = allEpisodes
|
val toDownload = downloadMediaSourceResolver.getUnwatchedEpisodeIds(
|
||||||
.filter { ep -> ep.userData?.played != true && ep.id !in unwatchedDownloaded }
|
seriesId = seriesId,
|
||||||
.take(needed)
|
excludedEpisodeIds = unwatchedDownloaded.toSet(),
|
||||||
.map { it.id }
|
limit = needed,
|
||||||
|
)
|
||||||
|
|
||||||
if (toDownload.isNotEmpty()) {
|
if (toDownload.isNotEmpty()) {
|
||||||
Log.d(TAG, "Smart download: queuing ${toDownload.size} episodes for series $seriesId")
|
Log.d(TAG, "Smart download: queuing ${toDownload.size} episodes for series $seriesId")
|
||||||
@@ -349,18 +267,12 @@ class MediaDownloadManager @Inject constructor(
|
|||||||
mediaId: UUID,
|
mediaId: UUID,
|
||||||
playbackUrl: String,
|
playbackUrl: String,
|
||||||
title: String,
|
title: String,
|
||||||
isDirectPlay: Boolean
|
customCacheKey: String?,
|
||||||
): DownloadRequest {
|
): DownloadRequest {
|
||||||
val builder = DownloadRequest.Builder(mediaId.toString(), playbackUrl.toUri())
|
val builder = DownloadRequest.Builder(mediaId.toString(), playbackUrl.toUri())
|
||||||
.setData(title.toByteArray(Charsets.UTF_8))
|
.setData(title.toByteArray(Charsets.UTF_8))
|
||||||
|
|
||||||
if (isDirectPlay) {
|
customCacheKey?.let(builder::setCustomCacheKey)
|
||||||
playbackCustomCacheKey(
|
|
||||||
mediaId = mediaId.toString(),
|
|
||||||
playbackUrl = playbackUrl,
|
|
||||||
playMethod = PlayMethod.DIRECT_PLAY
|
|
||||||
)?.let(builder::setCustomCacheKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
@@ -375,73 +287,8 @@ class MediaDownloadManager @Inject constructor(
|
|||||||
else -> DownloadState.NotDownloaded
|
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 {
|
companion object {
|
||||||
private const val TAG = "MediaDownloadManager"
|
private const val TAG = "MediaDownloadManager"
|
||||||
private const val SMART_DOWNLOAD_COUNT = 5
|
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.app.Notification
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -50,7 +50,7 @@ class PurefinDownloadService : DownloadService(
|
|||||||
if (activeDownloads.isEmpty()) {
|
if (activeDownloads.isEmpty()) {
|
||||||
return entryPoint.downloadNotificationHelper().buildProgressNotification(
|
return entryPoint.downloadNotificationHelper().buildProgressNotification(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_launcher_foreground,
|
android.R.drawable.stat_sys_download,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
downloads,
|
downloads,
|
||||||
@@ -95,7 +95,7 @@ class PurefinDownloadService : DownloadService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return NotificationCompat.Builder(this, DOWNLOAD_CHANNEL_ID)
|
return NotificationCompat.Builder(this, DOWNLOAD_CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setContentText(contentText)
|
.setContentText(contentText)
|
||||||
.setProgress(100, percent.toInt(), false)
|
.setProgress(100, percent.toInt(), false)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="download_channel_name">Downloads</string>
|
<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>
|
</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.DataSource
|
||||||
import androidx.media3.datasource.cache.CacheDataSource
|
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.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
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.Module
|
||||||
import dagger.Provides
|
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 androidx.navigation3.runtime.NavKey
|
||||||
import kotlinx.serialization.Serializable
|
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 java.util.UUID
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
@@ -31,6 +31,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":core:model"))
|
implementation(project(":core:model"))
|
||||||
implementation(project(":core:data"))
|
implementation(project(":core:data"))
|
||||||
|
implementation(project(":core:image"))
|
||||||
implementation(libs.hilt)
|
implementation(libs.hilt)
|
||||||
ksp(libs.hilt.compiler)
|
ksp(libs.hilt.compiler)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import androidx.media3.common.TrackSelectionOverride
|
|||||||
import androidx.media3.common.Tracks
|
import androidx.media3.common.Tracks
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
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.QueueItemUi
|
||||||
import hu.bbara.purefin.core.player.model.TrackOption
|
import hu.bbara.purefin.core.player.model.TrackOption
|
||||||
import hu.bbara.purefin.core.player.model.TrackType
|
import hu.bbara.purefin.core.player.model.TrackType
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.util.Log
|
|||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
import hu.bbara.purefin.core.data.MediaProgressWriter
|
import hu.bbara.purefin.core.data.MediaProgressWriter
|
||||||
import hu.bbara.purefin.core.data.PlaybackProgressReporter
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
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.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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user