From def92ad111c90bd5c12db84ae679cc806a0a432d Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Fri, 12 Jun 2026 17:47:14 +0000 Subject: [PATCH] feat(ui): add watched toggle to detail screens and home/library cards Add mark-as-watched/unwatched bottom sheet actions to home screen NextUp cards and library browse cards, and to library screen grid items. Add a single-button watched toggle (with dual icon/color states) to Movie, Episode, and Series detail screens. Inject JellyfinMediaMetadataUpdater into all four affected ViewModels. --- .../ui/common/media/MediaDetailScaffold.kt | 27 +++++++++++++++++++ .../ui/screen/episode/EpisodeScreen.kt | 12 ++++++++- .../episode/components/EpisodeComponents.kt | 1 + .../ui/screen/home/components/HomeContent.kt | 12 +++++---- .../home/components/library/HomeBrowseCard.kt | 13 +++++++++ .../library/LibraryPosterSection.kt | 2 ++ .../home/components/nextup/NextUpCard.kt | 16 +++++++++++ .../home/components/nextup/NextUpSection.kt | 2 ++ .../ui/screen/library/LibraryScreen.kt | 18 ++++++++++++- .../purefin/ui/screen/movie/MovieScreen.kt | 12 ++++++++- .../movie/components/MovieComponents.kt | 1 + .../purefin/ui/screen/series/SeriesScreen.kt | 11 ++++++++ .../series/components/SeriesComponents.kt | 1 + .../browse/library/LibraryViewModel.kt | 8 ++++++ .../content/episode/EpisodeScreenViewModel.kt | 9 +++++++ .../content/movie/MovieScreenViewModel.kt | 9 +++++++ .../feature/content/series/SeriesViewModel.kt | 9 +++++++ 17 files changed, 155 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaDetailScaffold.kt b/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaDetailScaffold.kt index 7cca2d23..169ee7ef 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaDetailScaffold.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/media/MediaDetailScaffold.kt @@ -21,7 +21,9 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.RadioButtonUnchecked import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -95,6 +97,12 @@ sealed interface MediaDetailSecondaryActionUiModel { val onClick: () -> Unit = {}, override val testTag: String? = null, ) : MediaDetailSecondaryActionUiModel + + data class MarkAsWatched( + val watched: Boolean, + val onClick: () -> Unit, + override val testTag: String? = null, + ) : MediaDetailSecondaryActionUiModel } data class MediaDetailSynopsisUiModel( @@ -356,6 +364,25 @@ private fun MediaDetailActions( modifier = Modifier.optionalTestTag(action.testTag) ) } + + is MediaDetailSecondaryActionUiModel.MarkAsWatched -> { + MediaActionButton( + backgroundColor = MaterialTheme.colorScheme.surface, + iconColor = if (action.watched) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + }, + icon = if (action.watched) { + Icons.Outlined.CheckCircle + } else { + Icons.Outlined.RadioButtonUnchecked + }, + height = 48.dp, + onClick = action.onClick, + modifier = Modifier.optionalTestTag(action.testTag) + ) + } } } } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt index 14f03cfc..8f4ef69a 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/episode/EpisodeScreen.kt @@ -34,6 +34,7 @@ import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress import hu.bbara.purefin.ui.common.permission.rememberNotificationPermissionGate import hu.bbara.purefin.ui.screen.episode.components.EpisodeDownloadButtonTag import hu.bbara.purefin.ui.screen.episode.components.EpisodePlayButtonTag +import hu.bbara.purefin.ui.screen.episode.components.EpisodeWatchedButtonTag import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBar import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBarShortcut import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen @@ -83,6 +84,7 @@ fun EpisodeScreen( downloadState = downloadState.value, onBack = viewModel::onBack, onDownloadClick = onDownloadClick, + onMarkAsWatched = viewModel::markAsWatched, modifier = modifier ) } @@ -95,6 +97,7 @@ private fun EpisodeScreenInternal( downloadState: DownloadState, onBack: () -> Unit, onDownloadClick: () -> Unit, + onMarkAsWatched: (Boolean) -> Unit = {}, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -110,7 +113,8 @@ private fun EpisodeScreenInternal( uiModel = episode.toMediaDetailScaffoldUiModel( onPlayClick = playAction, downloadState = downloadState, - onDownloadClick = onDownloadClick + onDownloadClick = onDownloadClick, + onMarkAsWatched = onMarkAsWatched, ), modifier = modifier, topBar = { scrollBehavior -> @@ -127,6 +131,7 @@ private fun Episode.toMediaDetailScaffoldUiModel( onPlayClick: () -> Unit, downloadState: DownloadState, onDownloadClick: () -> Unit, + onMarkAsWatched: (Boolean) -> Unit, ): MediaDetailScaffoldUiModel = MediaDetailScaffoldUiModel( imageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY), title = title, @@ -141,6 +146,11 @@ private fun Episode.toMediaDetailScaffoldUiModel( testTag = EpisodePlayButtonTag ), secondaryActions = listOf( + MediaDetailSecondaryActionUiModel.MarkAsWatched( + watched = watched, + onClick = { onMarkAsWatched(!watched) }, + testTag = EpisodeWatchedButtonTag + ), MediaDetailSecondaryActionUiModel.Download( downloadState = downloadState, onClick = onDownloadClick, diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/episode/components/EpisodeComponents.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/episode/components/EpisodeComponents.kt index dead44cd..978f7716 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/episode/components/EpisodeComponents.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/episode/components/EpisodeComponents.kt @@ -88,3 +88,4 @@ internal fun EpisodeTopBar( internal const val EpisodeSeriesButtonTag = "episode-series-button" internal const val EpisodePlayButtonTag = "episode-play-button" internal const val EpisodeDownloadButtonTag = "episode-download-button" +internal const val EpisodeWatchedButtonTag = "episode-watched-button" diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt index b5743256..6d41f811 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt @@ -121,11 +121,12 @@ fun HomeContent( if (nextUp.isNotEmpty()) { item(key = "next-up") { - NextUpSection( - items = nextUp, - onMediaSelected = onMediaSelected, - modifier = Modifier.testTag(HomeNextUpSectionTag) - ) + NextUpSection( + items = nextUp, + onMediaSelected = onMediaSelected, + onMarkAsWatched = onMarkAsWatched, + modifier = Modifier.testTag(HomeNextUpSectionTag) + ) } } @@ -138,6 +139,7 @@ fun HomeContent( items = libraryContent[library.id].orEmpty(), onLibrarySelected = onLibrarySelected, onMediaSelected = onMediaSelected, + onMarkAsWatched = onMarkAsWatched, modifier = Modifier.testTag("$HomeLibrarySectionTagPrefix${library.id}") ) } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/HomeBrowseCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/HomeBrowseCard.kt index ab86f432..7ff85838 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/HomeBrowseCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/HomeBrowseCard.kt @@ -10,28 +10,41 @@ import hu.bbara.purefin.core.model.EpisodeUiModel import hu.bbara.purefin.core.model.MediaUiModel import hu.bbara.purefin.core.model.MovieUiModel import hu.bbara.purefin.core.model.SeriesUiModel +import androidx.compose.runtime.remember import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeBadge import hu.bbara.purefin.ui.common.badge.WatchStateBadge import hu.bbara.purefin.ui.common.card.MediaImageCard import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsSource import hu.bbara.purefin.ui.common.media.rememberHomeMediaSharedBoundsClick +import hu.bbara.purefin.ui.model.MediaAction @Composable internal fun HomeBrowseCard( uiModel: MediaUiModel, sharedBoundsKey: String, onMediaSelected: (MediaUiModel) -> Unit, + onMarkAsWatched: (MediaUiModel, Boolean) -> Unit, modifier: Modifier = Modifier ) { val onClick = rememberHomeMediaSharedBoundsClick(sharedBoundsKey) { onMediaSelected(uiModel) } + val popupActions = remember(uiModel, onMarkAsWatched) { + buildList { + if (uiModel is MovieUiModel || uiModel is EpisodeUiModel || uiModel is SeriesUiModel) { + add(MediaAction(name = "Mark as watched") { onMarkAsWatched(uiModel, true) }) + add(MediaAction(name = "Mark as unwatched") { onMarkAsWatched(uiModel, false) }) + } + } + } + MediaImageCard( imageUrl = uiModel.primaryImageUrl, title = uiModel.primaryText, subtitle = uiModel.secondaryText, onClick = onClick, + popupActions = popupActions, imageModifier = Modifier.homeMediaSharedBoundsSource(sharedBoundsKey), imageAspectRatio = 15f / 16f, modifier = modifier.width(188.dp) diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/LibraryPosterSection.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/LibraryPosterSection.kt index e58b32e2..2ca18cd6 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/LibraryPosterSection.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/library/LibraryPosterSection.kt @@ -25,6 +25,7 @@ fun LibraryPosterSection( items: List, onLibrarySelected: (LibraryUiModel) -> Unit, onMediaSelected: (MediaUiModel) -> Unit, + onMarkAsWatched: (MediaUiModel, Boolean) -> Unit, modifier: Modifier = Modifier, ) { if (items.isEmpty()) return @@ -61,6 +62,7 @@ fun LibraryPosterSection( mediaId = item.id ), onMediaSelected = onMediaSelected, + onMarkAsWatched = onMarkAsWatched, modifier = Modifier.testTag("$HomeLibraryItemTagPrefix${library.id}-$index") ) } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpCard.kt index b0df7f02..f4d3efa5 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpCard.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -13,24 +14,39 @@ import androidx.compose.ui.unit.dp import hu.bbara.purefin.ui.common.card.MediaImageCard import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsSource import hu.bbara.purefin.ui.common.media.rememberHomeMediaSharedBoundsClick +import hu.bbara.purefin.ui.model.MediaAction import hu.bbara.purefin.core.model.MediaUiModel +import hu.bbara.purefin.core.model.MovieUiModel +import hu.bbara.purefin.core.model.EpisodeUiModel +import hu.bbara.purefin.core.model.SeriesUiModel @Composable internal fun NextUpCard( uiModel: MediaUiModel, sharedBoundsKey: String, onMediaSelected: (MediaUiModel) -> Unit, + onMarkAsWatched: (MediaUiModel, Boolean) -> Unit, modifier: Modifier = Modifier ) { val onClick = rememberHomeMediaSharedBoundsClick(sharedBoundsKey) { onMediaSelected(uiModel) } + val popupActions = remember(uiModel, onMarkAsWatched) { + buildList { + if (uiModel is MovieUiModel || uiModel is EpisodeUiModel || uiModel is SeriesUiModel) { + add(MediaAction(name = "Mark as watched") { onMarkAsWatched(uiModel, true) }) + add(MediaAction(name = "Mark as unwatched") { onMarkAsWatched(uiModel, false) }) + } + } + } + MediaImageCard( imageUrl = uiModel.primaryImageUrl, title = uiModel.primaryText, subtitle = uiModel.secondaryText, onClick = onClick, + popupActions = popupActions, imageModifier = Modifier.homeMediaSharedBoundsSource(sharedBoundsKey), shapeSize = 24.dp, titleStyle = MaterialTheme.typography.titleSmall, diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpSection.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpSection.kt index 2117b6b7..70996d28 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpSection.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/nextup/NextUpSection.kt @@ -22,6 +22,7 @@ import hu.bbara.purefin.core.model.MediaUiModel fun NextUpSection( items: List, onMediaSelected: (MediaUiModel) -> Unit, + onMarkAsWatched: (MediaUiModel, Boolean) -> Unit, modifier: Modifier = Modifier ) { if (items.isEmpty()) return @@ -51,6 +52,7 @@ fun NextUpSection( uiModel = item, sharedBoundsKey = homeMediaSharedBoundsKey("next-up-$index", item.id), onMediaSelected = onMediaSelected, + onMarkAsWatched = onMarkAsWatched, modifier = Modifier.testTag("$HomeNextUpItemTagPrefix$index") ) } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt index e766e77a..7e634540 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/library/LibraryScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -30,6 +31,7 @@ import hu.bbara.purefin.core.model.SeriesUiModel import hu.bbara.purefin.core.navigation.LibraryDto import hu.bbara.purefin.ui.common.badge.WatchStateBadge import hu.bbara.purefin.ui.common.card.MediaImageCard +import hu.bbara.purefin.ui.model.MediaAction import hu.bbara.purefin.ui.screen.home.components.rememberDefaultTopBarScrollBehavior import hu.bbara.purefin.ui.screen.library.components.LibraryTopBar @@ -66,7 +68,10 @@ fun LibraryScreen( .background(MaterialTheme.colorScheme.background) .padding(innerPadding) ) { - LibraryPosterGrid(libraryItems = libraryItems.value) + LibraryPosterGrid( + libraryItems = libraryItems.value, + onMarkAsWatched = viewModel::markAsWatched, + ) } } } @@ -75,6 +80,7 @@ fun LibraryScreen( internal fun LibraryPosterGrid( libraryItems: List, modifier: Modifier = Modifier, + onMarkAsWatched: (MediaUiModel, Boolean) -> Unit, viewModel: LibraryViewModel = hiltViewModel() ) { BoxWithConstraints( @@ -91,6 +97,15 @@ internal fun LibraryPosterGrid( verticalArrangement = Arrangement.spacedBy(16.dp), ) { items(libraryItems, key = { item -> item.id }) { item -> + val popupActions = remember { + buildList { + if (item is MovieUiModel || item is EpisodeUiModel || item is SeriesUiModel) { + add(MediaAction(name = "Mark as watched") { onMarkAsWatched(item, true) }) + add(MediaAction(name = "Mark as unwatched") { onMarkAsWatched(item, false) }) + } + } + } + MediaImageCard( imageUrl = item.primaryImageUrl, title = item.primaryText, @@ -102,6 +117,7 @@ internal fun LibraryPosterGrid( is EpisodeUiModel -> Unit } }, + popupActions = popupActions, modifier = Modifier.testTag("$LibraryPosterItemTagPrefix${item.id}") ) { when (item) { diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt index 8d847ed7..c446c861 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/movie/MovieScreen.kt @@ -28,6 +28,7 @@ import hu.bbara.purefin.ui.common.media.MediaDetailScaffoldUiModel import hu.bbara.purefin.ui.common.media.MediaDetailSecondaryActionUiModel import hu.bbara.purefin.ui.common.media.MediaDetailSynopsisUiModel import hu.bbara.purefin.ui.common.media.mediaPlayButtonText +import hu.bbara.purefin.ui.screen.movie.components.MovieWatchedButtonTag import hu.bbara.purefin.ui.common.media.mediaPlaybackProgress import hu.bbara.purefin.ui.common.permission.rememberNotificationPermissionGate import hu.bbara.purefin.ui.screen.movie.components.MovieDownloadButtonTag @@ -67,6 +68,7 @@ fun MovieScreen( downloadState = downloadState.value, onDownloadClick = onDownloadClick, onBack = viewModel::onBack, + onMarkAsWatched = viewModel::markAsWatched, modifier = modifier ) } else { @@ -81,6 +83,7 @@ private fun MovieScreenInternal( downloadState: DownloadState = DownloadState.NotDownloaded, onDownloadClick: () -> Unit = {}, onBack: () -> Unit, + onMarkAsWatched: (Boolean) -> Unit = {}, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -96,7 +99,8 @@ private fun MovieScreenInternal( uiModel = movie.toMediaDetailScaffoldUiModel( onPlayClick = playAction, downloadState = downloadState, - onDownloadClick = onDownloadClick + onDownloadClick = onDownloadClick, + onMarkAsWatched = onMarkAsWatched, ), modifier = modifier, topBar = { scrollBehavior -> @@ -112,6 +116,7 @@ private fun Movie.toMediaDetailScaffoldUiModel( onPlayClick: () -> Unit, downloadState: DownloadState, onDownloadClick: () -> Unit, + onMarkAsWatched: (Boolean) -> Unit, ): MediaDetailScaffoldUiModel = MediaDetailScaffoldUiModel( imageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY), title = title, @@ -125,6 +130,11 @@ private fun Movie.toMediaDetailScaffoldUiModel( testTag = MoviePlayButtonTag ), secondaryActions = listOf( + MediaDetailSecondaryActionUiModel.MarkAsWatched( + watched = watched, + onClick = { onMarkAsWatched(!watched) }, + testTag = MovieWatchedButtonTag + ), MediaDetailSecondaryActionUiModel.Download( downloadState = downloadState, onClick = onDownloadClick, diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/movie/components/MovieComponents.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/movie/components/MovieComponents.kt index f04fd0cb..484b183a 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/movie/components/MovieComponents.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/movie/components/MovieComponents.kt @@ -38,3 +38,4 @@ internal fun MovieTopBar( internal const val MoviePlayButtonTag = "movie-play-button" internal const val MovieDownloadButtonTag = "movie-download-button" +internal const val MovieWatchedButtonTag = "movie-watched-button" diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt index 8a1dee75..24cd4eb4 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/series/SeriesScreen.kt @@ -48,6 +48,7 @@ import hu.bbara.purefin.ui.screen.series.components.SeasonTabs import hu.bbara.purefin.ui.screen.series.components.SeriesAddButtonTag import hu.bbara.purefin.ui.screen.series.components.SeriesDownloadButtonTag import hu.bbara.purefin.ui.screen.series.components.SeriesDownloadOption +import hu.bbara.purefin.ui.screen.series.components.SeriesWatchedButtonTag import hu.bbara.purefin.ui.screen.series.components.SeriesPlayButtonTag import hu.bbara.purefin.ui.screen.series.components.SeriesTopBar import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen @@ -117,6 +118,7 @@ fun SeriesScreen( onLoadSeasonEpisodes = viewModel::loadSeasonEpisodes, onObserveSeasonDownloadState = viewModel::observeSeasonDownloadState, onBack = viewModel::onGoHome, + onMarkAsWatched = viewModel::markAsWatched, offline = series.offline, modifier = modifier ) @@ -136,6 +138,7 @@ private fun SeriesScreenInternal( onLoadSeasonEpisodes: (UUID, UUID) -> Unit, onObserveSeasonDownloadState: (List) -> Unit, onBack: () -> Unit, + onMarkAsWatched: (Boolean) -> Unit = {}, offline: Boolean, modifier: Modifier = Modifier, ) { @@ -187,6 +190,7 @@ private fun SeriesScreenInternal( nextUpEpisode = nextUpEpisode, onPlayClick = playAction ?: {}, onDownloadClick = { showDownloadDialog = true }, + onMarkAsWatched = onMarkAsWatched, bodyColor = scheme.onSurface ), modifier = modifier, @@ -238,6 +242,7 @@ private fun Series.toMediaDetailScaffoldUiModel( nextUpEpisode: Episode?, onPlayClick: () -> Unit, onDownloadClick: () -> Unit, + onMarkAsWatched: (Boolean) -> Unit, bodyColor: Color, ): MediaDetailScaffoldUiModel = MediaDetailScaffoldUiModel( imageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY), @@ -246,6 +251,7 @@ private fun Series.toMediaDetailScaffoldUiModel( titleLineHeight = 36.sp, metadataItems = listOf(year, "$seasonCount Seasons"), actions = selectedSeason?.let { + val seriesWatched = unwatchedEpisodeCount == 0 MediaDetailActionsUiModel( primaryAction = MediaDetailPrimaryActionUiModel( text = mediaPlayButtonText(nextUpEpisode?.progress, nextUpEpisode?.watched), @@ -254,6 +260,11 @@ private fun Series.toMediaDetailScaffoldUiModel( testTag = SeriesPlayButtonTag ), secondaryActions = listOf( + MediaDetailSecondaryActionUiModel.MarkAsWatched( + watched = seriesWatched, + onClick = { onMarkAsWatched(!seriesWatched) }, + testTag = SeriesWatchedButtonTag + ), MediaDetailSecondaryActionUiModel.Icon( icon = Icons.Outlined.Add, testTag = SeriesAddButtonTag diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/series/components/SeriesComponents.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/series/components/SeriesComponents.kt index 90cf82b0..87e8467e 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/series/components/SeriesComponents.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/series/components/SeriesComponents.kt @@ -394,6 +394,7 @@ private fun EpisodeCard( internal const val SeriesPlayButtonTag = "series-play-button" internal const val SeriesAddButtonTag = "series-add-button" internal const val SeriesDownloadButtonTag = "series-download-button" +internal const val SeriesWatchedButtonTag = "series-watched-button" internal const val SeriesDownloadDialogTag = "series-download-dialog" internal const val SeriesDownloadSeasonButtonTag = "series-download-season-button" internal const val SeriesDownloadAllButtonTag = "series-download-all-button" diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/browse/library/LibraryViewModel.kt b/core/src/main/java/hu/bbara/purefin/core/feature/browse/library/LibraryViewModel.kt index 139b4e70..da7c2b4c 100644 --- a/core/src/main/java/hu/bbara/purefin/core/feature/browse/library/LibraryViewModel.kt +++ b/core/src/main/java/hu/bbara/purefin/core/feature/browse/library/LibraryViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import hu.bbara.purefin.core.data.HomeRepository +import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater import hu.bbara.purefin.core.model.MediaUiModel import hu.bbara.purefin.core.model.MovieUiModel import hu.bbara.purefin.core.model.SeriesUiModel @@ -25,6 +26,7 @@ import javax.inject.Inject class LibraryViewModel @Inject constructor( private val homeRepository: HomeRepository, private val navigationManager: NavigationManager, + private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater, ) : ViewModel() { private val selectedLibrary = MutableStateFlow(null) @@ -69,6 +71,12 @@ class LibraryViewModel @Inject constructor( } } + fun markAsWatched(mediaUiModel: MediaUiModel, watched: Boolean) { + viewModelScope.launch { + jellyfinMediaMetadataUpdater.markAsWatched(mediaUiModel.id, watched) + } + } + fun onBack() { navigationManager.pop() } diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/content/episode/EpisodeScreenViewModel.kt b/core/src/main/java/hu/bbara/purefin/core/feature/content/episode/EpisodeScreenViewModel.kt index 12c5a545..41b764a5 100644 --- a/core/src/main/java/hu/bbara/purefin/core/feature/content/episode/EpisodeScreenViewModel.kt +++ b/core/src/main/java/hu/bbara/purefin/core/feature/content/episode/EpisodeScreenViewModel.kt @@ -7,6 +7,7 @@ import hu.bbara.purefin.core.Offline import hu.bbara.purefin.core.data.LocalMediaRepository import hu.bbara.purefin.core.download.DownloadState import hu.bbara.purefin.core.download.MediaDownloadController +import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater import hu.bbara.purefin.core.navigation.EpisodeDto import hu.bbara.purefin.core.navigation.NavigationManager import hu.bbara.purefin.core.navigation.Route @@ -30,6 +31,7 @@ class EpisodeScreenViewModel @Inject constructor( @param:Offline private val offlineMediaCatalogReader: LocalMediaRepository, private val navigationManager: NavigationManager, private val mediaDownloadManager: MediaDownloadController, + private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater, ): ViewModel() { private val _episode = MutableStateFlow(null) @@ -81,6 +83,13 @@ class EpisodeScreenViewModel @Inject constructor( } } + fun markAsWatched(watched: Boolean) { + val episodeId = _episode.value?.id ?: return + viewModelScope.launch { + jellyfinMediaMetadataUpdater.markAsWatched(episodeId, watched) + } + } + fun onDownloadClick() { val episodeId = _episode.value?.id ?: return viewModelScope.launch { diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/content/movie/MovieScreenViewModel.kt b/core/src/main/java/hu/bbara/purefin/core/feature/content/movie/MovieScreenViewModel.kt index a9176cf7..67bc9abb 100644 --- a/core/src/main/java/hu/bbara/purefin/core/feature/content/movie/MovieScreenViewModel.kt +++ b/core/src/main/java/hu/bbara/purefin/core/feature/content/movie/MovieScreenViewModel.kt @@ -7,6 +7,7 @@ import hu.bbara.purefin.core.Offline import hu.bbara.purefin.core.data.LocalMediaRepository import hu.bbara.purefin.core.download.DownloadState import hu.bbara.purefin.core.download.MediaDownloadController +import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater import hu.bbara.purefin.core.navigation.MovieDto import hu.bbara.purefin.core.navigation.NavigationManager import hu.bbara.purefin.core.navigation.Route @@ -28,6 +29,7 @@ class MovieScreenViewModel @Inject constructor( @param:Offline private val offlineMediaCatalogReader: LocalMediaRepository, private val navigationManager: NavigationManager, private val mediaDownloadManager: MediaDownloadController, + private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater, ): ViewModel() { private val _movie = MutableStateFlow(null) @@ -68,6 +70,13 @@ class MovieScreenViewModel @Inject constructor( } } + fun markAsWatched(watched: Boolean) { + val movieId = movie.value?.id ?: return + viewModelScope.launch { + jellyfinMediaMetadataUpdater.markAsWatched(movieId, watched) + } + } + fun onDownloadClick() { val movieId = movie.value?.id ?: return viewModelScope.launch { diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/content/series/SeriesViewModel.kt b/core/src/main/java/hu/bbara/purefin/core/feature/content/series/SeriesViewModel.kt index 90cfcc03..2c378d26 100644 --- a/core/src/main/java/hu/bbara/purefin/core/feature/content/series/SeriesViewModel.kt +++ b/core/src/main/java/hu/bbara/purefin/core/feature/content/series/SeriesViewModel.kt @@ -7,6 +7,7 @@ import hu.bbara.purefin.core.Offline import hu.bbara.purefin.core.data.LocalMediaRepository import hu.bbara.purefin.core.download.DownloadState import hu.bbara.purefin.core.download.MediaDownloadController +import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater import hu.bbara.purefin.core.navigation.EpisodeDto import hu.bbara.purefin.core.navigation.NavigationManager import hu.bbara.purefin.core.navigation.Route @@ -33,6 +34,7 @@ class SeriesViewModel @Inject constructor( @param:Offline private val offlineMediaCatalogReader: LocalMediaRepository, private val navigationManager: NavigationManager, private val mediaDownloadManager: MediaDownloadController, + private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater, ) : ViewModel() { private val _series = MutableStateFlow(null) @@ -183,6 +185,13 @@ class SeriesViewModel @Inject constructor( navigationManager.replaceAll(Route.Home) } + fun markAsWatched(watched: Boolean) { + val seriesId = _series.value?.id ?: return + viewModelScope.launch { + jellyfinMediaMetadataUpdater.markAsWatched(seriesId, watched) + } + } + fun selectSeries(series: SeriesDto) { _series.value = series viewModelScope.launch {