From 71400d1654c7ac7674c1fa3b9acf090417fbe7c1 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Thu, 7 May 2026 14:29:15 +0200 Subject: [PATCH 1/7] add bookmark badge --- .../app/core/ui/NuvioPosterActionSheet.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPosterActionSheet.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPosterActionSheet.kt index e226f637..5a617dca 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPosterActionSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPosterActionSheet.kt @@ -38,6 +38,7 @@ import com.nuvio.app.core.format.formatReleaseDateForDisplay import com.nuvio.app.features.home.MetaPreview import kotlinx.coroutines.launch import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.action_saved import nuvio.composeapp.generated.resources.episodes_cd_watched import nuvio.composeapp.generated.resources.hero_add_to_library import nuvio.composeapp.generated.resources.hero_mark_unwatched @@ -151,6 +152,41 @@ fun NuvioAnimatedWatchedBadge( } } +@Composable +fun NuvioBookmarkedBadge( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(22.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.Bookmark, + contentDescription = stringResource(Res.string.action_saved), + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(12.dp), + ) + } +} + +@Composable +fun NuvioAnimatedBookmarkedBadge( + isVisible: Boolean, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = isVisible, + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier, + ) { + NuvioBookmarkedBadge() + } +} + @Composable private fun PosterSheetHeader( item: MetaPreview, From 219210fd64f4d617a8fb8d80f8e791f5d183c6f3 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Thu, 7 May 2026 14:31:46 +0200 Subject: [PATCH 2/7] add bookmark badge to poster component --- .../nuvio/app/core/ui/NuvioShelfComponents.kt | 8 ++++++++ .../features/home/components/HomePosterCard.kt | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioShelfComponents.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioShelfComponents.kt index ace10d77..d07b4c1e 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioShelfComponents.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioShelfComponents.kt @@ -107,6 +107,7 @@ fun NuvioPosterCard( bottomLeftLogoUrl: String? = null, bottomLeftText: String? = null, isWatched: Boolean = false, + isSaved: Boolean = false, onClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, ) { @@ -185,6 +186,13 @@ fun NuvioPosterCard( .align(Alignment.TopEnd) .padding(6.dp), ) + + NuvioAnimatedBookmarkedBadge( + isVisible = isSaved, + modifier = Modifier + .align(Alignment.TopStart) + .padding(6.dp), + ) } if (shouldShowTitleBelow) { Text( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt index fc8b4133..d917163c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt @@ -1,13 +1,17 @@ package com.nuvio.app.features.home.components import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nuvio.app.core.format.formatReleaseDateForDisplay import com.nuvio.app.core.ui.NuvioPosterCard import com.nuvio.app.core.ui.NuvioPosterShape import com.nuvio.app.core.ui.rememberPosterCardStyleUiState import com.nuvio.app.features.home.MetaPreview import com.nuvio.app.features.home.PosterShape +import com.nuvio.app.features.library.LibraryRepository @Composable fun HomePosterCard( @@ -18,6 +22,19 @@ fun HomePosterCard( onClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, ) { + val libraryUiState by remember { + LibraryRepository.ensureLoaded() + LibraryRepository.uiState + }.collectAsStateWithLifecycle() + val isSaved = remember( + libraryUiState.items, + libraryUiState.sections, + libraryUiState.sourceMode, + item.id, + item.type, + ) { + LibraryRepository.isSaved(item.id, item.type) + } val posterCardStyle = rememberPosterCardStyleUiState() val isLandscapeMode = useLandscapeBackdropMode || posterCardStyle.catalogLandscapeModeEnabled @@ -31,6 +48,7 @@ fun HomePosterCard( bottomLeftLogoUrl = if (isLandscapeMode) item.logo else null, bottomLeftText = if (isLandscapeMode && item.logo.isNullOrBlank() && !posterCardStyle.hideLabelsEnabled) item.name else null, isWatched = isWatched, + isSaved = isSaved, onClick = onClick, onLongClick = onLongClick, ) From 7be85691ed8f932fe0649153bb3995dc5813420d Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Thu, 7 May 2026 14:39:55 +0200 Subject: [PATCH 3/7] add bookmark badge in catalog screen --- .../app/features/catalog/CatalogScreen.kt | 25 +++++++++++++++++++ .../features/collection/FolderDetailScreen.kt | 16 ++++++++++++ 2 files changed, 41 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt index f58cd2df..e0bbc593 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt @@ -46,6 +46,8 @@ import com.nuvio.app.core.network.NetworkStatusRepository import com.nuvio.app.core.ui.NuvioNetworkOfflineCard import coil3.compose.AsyncImage import com.nuvio.app.core.format.formatReleaseDateForDisplay +import com.nuvio.app.core.ui.NuvioAnimatedBookmarkedBadge +import com.nuvio.app.core.ui.NuvioAnimatedWatchedBadge import com.nuvio.app.core.ui.NuvioBackButton import com.nuvio.app.core.ui.rememberPosterCardStyleUiState import com.nuvio.app.core.ui.posterCardClickable @@ -55,6 +57,7 @@ import com.nuvio.app.features.home.MetaPreview import com.nuvio.app.features.home.HomeCatalogSettingsRepository import com.nuvio.app.features.home.PosterShape import com.nuvio.app.features.home.stableKey +import com.nuvio.app.features.library.LibraryRepository import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -81,6 +84,10 @@ fun CatalogScreen( val gridState = rememberLazyGridState() var headerHeightPx by remember { mutableIntStateOf(0) } var observedOfflineState by remember { mutableStateOf(false) } + val libraryUiState by remember { + LibraryRepository.ensureLoaded() + LibraryRepository.uiState + }.collectAsStateWithLifecycle() LaunchedEffect(manifestUrl, type, catalogId, genre, supportsPagination, homeCatalogSettingsUiState.hideUnreleasedContent) { CatalogRepository.load( @@ -182,10 +189,20 @@ fun CatalogScreen( key = { item -> item.lazyKey }, ) { keyedItem -> val item = keyedItem.value + val isSaved = remember( + libraryUiState.items, + libraryUiState.sections, + libraryUiState.sourceMode, + item.id, + item.type, + ) { + LibraryRepository.isSaved(item.id, item.type) + } CatalogPosterTile( item = item, cornerRadiusDp = posterCardStyle.cornerRadiusDp, hideLabels = posterCardStyle.hideLabelsEnabled, + isSaved = isSaved, onClick = onPosterClick?.let { { it(item) } }, ) } @@ -256,6 +273,7 @@ private fun CatalogPosterTile( item: MetaPreview, cornerRadiusDp: Int, hideLabels: Boolean, + isSaved: Boolean = false, onClick: (() -> Unit)? = null, ) { Column( @@ -277,6 +295,13 @@ private fun CatalogPosterTile( contentScale = ContentScale.Crop, ) } + + NuvioAnimatedBookmarkedBadge( + isVisible = isSaved, + modifier = Modifier + .align(Alignment.TopStart) + .padding(6.dp), + ) } if (!hideLabels) { Text( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt index 6101d18a..e98bd314 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.nuvio.app.core.ui.NuvioPosterCard import com.nuvio.app.core.ui.NuvioPosterShape @@ -61,6 +62,7 @@ import com.nuvio.app.features.home.PosterShape import com.nuvio.app.features.home.canOpenCatalog import com.nuvio.app.features.home.stableKey import com.nuvio.app.features.home.components.HomeCatalogRowSection +import com.nuvio.app.features.library.LibraryRepository import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -204,6 +206,10 @@ private fun TabbedGridContent( onPosterClick: (MetaPreview) -> Unit, ) { val gridState = rememberLazyGridState() + val libraryUiState by remember { + LibraryRepository.ensureLoaded() + LibraryRepository.uiState + }.collectAsStateWithLifecycle() LaunchedEffect(gridState, uiState.selectedTabIndex, uiState.selectedTabCanLoadMore, uiState.selectedTabIsLoadingMore) { snapshotFlow { gridState.layoutInfo } @@ -280,11 +286,21 @@ private fun TabbedGridContent( key = { item -> item.lazyKey }, ) { keyedItem -> val item = keyedItem.value + val isSaved = remember( + libraryUiState.items, + libraryUiState.sections, + libraryUiState.sourceMode, + item.id, + item.type, + ) { + LibraryRepository.isSaved(item.id, item.type) + } NuvioPosterCard( title = item.name, imageUrl = item.poster, shape = NuvioPosterShape.Poster, detailLine = item.releaseInfo, + isSaved = isSaved, onClick = { onPosterClick(item) }, ) } From 03e4dd38099148f5dc4f35bc31cf74400f2ccf20 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Thu, 7 May 2026 14:44:50 +0200 Subject: [PATCH 4/7] add watched badge to catalog screen --- .../nuvio/app/features/catalog/CatalogScreen.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt index e0bbc593..e69cb3fc 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt @@ -58,6 +58,8 @@ import com.nuvio.app.features.home.HomeCatalogSettingsRepository import com.nuvio.app.features.home.PosterShape import com.nuvio.app.features.home.stableKey import com.nuvio.app.features.library.LibraryRepository +import com.nuvio.app.features.watched.WatchedRepository +import com.nuvio.app.features.watching.application.WatchingState import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -88,6 +90,7 @@ fun CatalogScreen( LibraryRepository.ensureLoaded() LibraryRepository.uiState }.collectAsStateWithLifecycle() + val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle() LaunchedEffect(manifestUrl, type, catalogId, genre, supportsPagination, homeCatalogSettingsUiState.hideUnreleasedContent) { CatalogRepository.load( @@ -203,6 +206,10 @@ fun CatalogScreen( cornerRadiusDp = posterCardStyle.cornerRadiusDp, hideLabels = posterCardStyle.hideLabelsEnabled, isSaved = isSaved, + isWatched = WatchingState.isPosterWatched( + watchedKeys = watchedUiState.watchedKeys, + item = item, + ), onClick = onPosterClick?.let { { it(item) } }, ) } @@ -273,6 +280,7 @@ private fun CatalogPosterTile( item: MetaPreview, cornerRadiusDp: Int, hideLabels: Boolean, + isWatched: Boolean = false, isSaved: Boolean = false, onClick: (() -> Unit)? = null, ) { @@ -296,6 +304,13 @@ private fun CatalogPosterTile( ) } + NuvioAnimatedWatchedBadge( + isVisible = isWatched, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp), + ) + NuvioAnimatedBookmarkedBadge( isVisible = isSaved, modifier = Modifier From 4804bc6966504281af420b625a3bd8bb00f17226 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Thu, 7 May 2026 14:46:20 +0200 Subject: [PATCH 5/7] add watched badge in folder details --- .../nuvio/app/features/collection/FolderDetailScreen.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt index e98bd314..593194cb 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt @@ -63,6 +63,8 @@ import com.nuvio.app.features.home.canOpenCatalog import com.nuvio.app.features.home.stableKey import com.nuvio.app.features.home.components.HomeCatalogRowSection import com.nuvio.app.features.library.LibraryRepository +import com.nuvio.app.features.watched.WatchedRepository +import com.nuvio.app.features.watching.application.WatchingState import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -210,6 +212,7 @@ private fun TabbedGridContent( LibraryRepository.ensureLoaded() LibraryRepository.uiState }.collectAsStateWithLifecycle() + val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle() LaunchedEffect(gridState, uiState.selectedTabIndex, uiState.selectedTabCanLoadMore, uiState.selectedTabIsLoadingMore) { snapshotFlow { gridState.layoutInfo } @@ -300,6 +303,10 @@ private fun TabbedGridContent( imageUrl = item.poster, shape = NuvioPosterShape.Poster, detailLine = item.releaseInfo, + isWatched = WatchingState.isPosterWatched( + watchedKeys = watchedUiState.watchedKeys, + item = item, + ), isSaved = isSaved, onClick = { onPosterClick(item) }, ) From 52b2d232ded46705035debae2f2778cf951c7566 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Thu, 7 May 2026 15:19:10 +0200 Subject: [PATCH 6/7] add watched badge in library screen --- .../com/nuvio/app/features/library/LibraryScreen.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt index efe6ded9..1d0899a0 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt @@ -31,6 +31,8 @@ import com.nuvio.app.features.home.components.HomeEmptyStateCard import com.nuvio.app.features.home.components.HomePosterCard import com.nuvio.app.features.home.components.HomeSkeletonRow import com.nuvio.app.features.profiles.ProfileRepository +import com.nuvio.app.features.watched.WatchedRepository +import com.nuvio.app.features.watching.application.WatchingState import kotlinx.coroutines.launch import nuvio.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -56,6 +58,7 @@ fun LibraryScreen( LibraryRepository.pullFromServer(ProfileRepository.activeProfileId) } } + val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle() LaunchedEffect(networkStatusUiState.condition, isTraktSource) { when (networkStatusUiState.condition) { @@ -162,6 +165,7 @@ fun LibraryScreen( else -> { librarySections( + watchedKeys = watchedUiState.watchedKeys, sections = uiState.sections, onPosterClick = onPosterClick, onSectionViewAllClick = onSectionViewAllClick, @@ -193,6 +197,7 @@ fun LibraryScreen( private fun LazyListScope.librarySections( sections: List, + watchedKeys: Set, onPosterClick: ((LibraryItem) -> Unit)?, onSectionViewAllClick: ((LibrarySection) -> Unit)?, onPosterLongClick: (LibraryItem) -> Unit, @@ -216,6 +221,10 @@ private fun LazyListScope.librarySections( key = { item -> "${item.type}:${item.id}" }, ) { item -> HomePosterCard( + isWatched = WatchingState.isPosterWatched( + watchedKeys = watchedKeys, + item = item.toMetaPreview(), + ), item = item.toMetaPreview(), onClick = onPosterClick?.let { { it(item) } }, onLongClick = { onPosterLongClick(item) }, From fa991511836701d54da3d0964a1bf14ea8022c68 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Thu, 7 May 2026 15:19:38 +0200 Subject: [PATCH 7/7] add watched badge to collection details --- .../com/nuvio/app/features/collection/FolderDetailScreen.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt index 593194cb..a8fb357f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailScreen.kt @@ -332,6 +332,7 @@ private fun RowsContent( onPosterClick: (MetaPreview) -> Unit, ) { val sections = FolderDetailRepository.getCatalogSectionsForRows() + val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle() if (uiState.isLoading && sections.isEmpty()) { LoadingIndicator() @@ -358,6 +359,7 @@ private fun RowsContent( HomeCatalogRowSection( section = section, entries = section.items.take(18), + watchedKeys = watchedUiState.watchedKeys, onViewAllClick = if (section.canOpenCatalog(18)) { { onCatalogClick(section) } } else {