From 485e1d2c651751618fb3376608ca33fb73922812 Mon Sep 17 00:00:00 2001 From: AdityasahuX07 Date: Wed, 22 Apr 2026 19:32:23 +0530 Subject: [PATCH 1/6] Remove pending removal item functionality Removed pending removal item logic and modal from LibraryScreen. --- .../app/features/library/LibraryScreen.kt | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) 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 1f86203f..61fd57f3 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 @@ -37,6 +37,7 @@ import kotlinx.coroutines.launch fun LibraryScreen( modifier: Modifier = Modifier, onPosterClick: ((LibraryItem) -> Unit)? = null, + onPosterLongClick: ((LibraryItem) -> Unit)? = null, onSectionViewAllClick: ((LibrarySection) -> Unit)? = null, ) { val uiState by remember { @@ -44,11 +45,12 @@ fun LibraryScreen( LibraryRepository.uiState }.collectAsStateWithLifecycle() val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle() - var pendingRemovalItem by remember { mutableStateOf(null) } +// var pendingRemovalItem by remember { mutableStateOf(null) } var observedOfflineState by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() val isTraktSource = uiState.sourceMode == LibrarySourceMode.TRAKT + LaunchedEffect(networkStatusUiState.condition, isTraktSource) { when (networkStatusUiState.condition) { NetworkCondition.NoInternet, @@ -156,27 +158,12 @@ fun LibraryScreen( onPosterClick = onPosterClick, onSectionViewAllClick = onSectionViewAllClick, onPosterLongClick = { item -> - if (!isTraktSource) { - pendingRemovalItem = item - } + onPosterLongClick?.invoke(item) }, ) } } } - - NuvioStatusModal( - title = "Remove from Library?", - message = pendingRemovalItem?.let { "Remove ${it.name} from your library?" }.orEmpty(), - isVisible = pendingRemovalItem != null, - confirmText = "Remove", - dismissText = "Cancel", - onConfirm = { - pendingRemovalItem?.id?.let(LibraryRepository::remove) - pendingRemovalItem = null - }, - onDismiss = { pendingRemovalItem = null }, - ) } private fun LazyListScope.librarySections( From 52ffd0c55359db5352d82ebf9e16581b7683d68e Mon Sep 17 00:00:00 2001 From: AdityasahuX07 Date: Wed, 22 Apr 2026 19:49:30 +0530 Subject: [PATCH 2/6] Update App.kt --- composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index f3b35366..6e42465a 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -114,6 +114,7 @@ import com.nuvio.app.features.library.LibrarySection import com.nuvio.app.features.library.LibrarySourceMode import com.nuvio.app.features.library.LibraryScreen import com.nuvio.app.features.library.toLibraryItem +import com.nuvio.app.features.library.toMetaPreview import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepository import com.nuvio.app.features.player.PlayerLaunch import com.nuvio.app.features.player.PlayerLaunchStore @@ -945,6 +946,10 @@ private fun MainAppContent( onLibraryPosterClick = { item -> navController.navigate(DetailRoute(type = item.type, id = item.id)) }, + onLibraryPosterLongClick = { item -> + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + selectedPosterForActions = item.toMetaPreview() // reuse existing sheet + }, onLibrarySectionViewAllClick = onLibrarySectionViewAllClick, onContinueWatchingClick = onContinueWatchingClick, onContinueWatchingLongPress = onContinueWatchingLongPress, @@ -1864,6 +1869,7 @@ private fun AppTabHost( onCollectionsSettingsClick: () -> Unit = {}, onFolderClick: ((collectionId: String, folderId: String) -> Unit)? = null, onInitialHomeContentRendered: () -> Unit = {}, + onLibraryPosterLongClick: ((LibraryItem) -> Unit)? = null, ) { val tabStateHolder = rememberSaveableStateHolder() @@ -1895,6 +1901,7 @@ private fun AppTabHost( LibraryScreen( modifier = Modifier.fillMaxSize(), onPosterClick = onLibraryPosterClick, + onPosterLongClick = onLibraryPosterLongClick, onSectionViewAllClick = onLibrarySectionViewAllClick, ) } From 8187f404c4cfff15a22a97ab30bb0b13b1ff246a Mon Sep 17 00:00:00 2001 From: AdityasahuX07 Date: Sat, 25 Apr 2026 13:10:39 +0530 Subject: [PATCH 3/6] Update App.kt --- .../commonMain/kotlin/com/nuvio/app/App.kt | 937 +++++++++--------- 1 file changed, 477 insertions(+), 460 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 6e42465a..0f3fd183 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -92,6 +92,7 @@ import com.nuvio.app.core.ui.NuvioToastController import com.nuvio.app.core.ui.NuvioFloatingPrompt import com.nuvio.app.core.ui.TraktListPickerDialog import com.nuvio.app.core.ui.NuvioTheme +import com.nuvio.app.core.ui.localizedContinueWatchingSubtitle import com.nuvio.app.features.auth.AuthScreen import com.nuvio.app.features.addons.AddonRepository import com.nuvio.app.features.catalog.CatalogRepository @@ -168,12 +169,20 @@ import com.nuvio.app.features.watching.application.WatchingState import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.* import nuvio.composeapp.generated.resources.app_logo_wordmark +import nuvio.composeapp.generated.resources.compose_catalog_subtitle_library +import nuvio.composeapp.generated.resources.compose_catalog_subtitle_trakt_library +import nuvio.composeapp.generated.resources.compose_nav_home +import nuvio.composeapp.generated.resources.compose_nav_library +import nuvio.composeapp.generated.resources.compose_nav_profile +import nuvio.composeapp.generated.resources.compose_nav_search import nuvio.composeapp.generated.resources.sidebar_library import nuvio.composeapp.generated.resources.sidebar_search import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource @Serializable object TabsRoute @@ -279,7 +288,6 @@ fun App() { ThemeSettingsRepository.selectedTheme }.collectAsStateWithLifecycle() val amoledEnabled by remember { ThemeSettingsRepository.amoledEnabled }.collectAsStateWithLifecycle() - NuvioTheme(appTheme = selectedTheme, amoled = amoledEnabled) { LaunchedEffect(Unit) { AuthRepository.initialize() @@ -325,8 +333,8 @@ fun App() { val cachedProfiles = profileState.profiles val allowOfflineProfileAccess = cachedProfiles.isNotEmpty() && - authState !is AuthState.Authenticated && - networkStatusUiState.condition != NetworkCondition.Online + authState !is AuthState.Authenticated && + networkStatusUiState.condition != NetworkCondition.Online when (authState) { is AuthState.Loading -> { @@ -444,47 +452,47 @@ fun App() { private fun MainAppContent( onSwitchProfile: () -> Unit = {}, ) { - val navController = rememberNavController() - val appUpdaterController = rememberAppUpdaterController() - remember { - EpisodeReleaseNotificationsRepository.ensureLoaded() - } - remember { - CollectionSyncService.startObserving() - } - remember { - HomeCatalogSettingsSyncService.startObserving() - } - remember { - ProfileSettingsSync.startObserving() - } - val hapticFeedback = LocalHapticFeedback.current - val coroutineScope = rememberCoroutineScope() - var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) } - var showExitConfirmation by rememberSaveable { mutableStateOf(false) } - var selectedPosterForActions by remember { mutableStateOf(null) } - var selectedContinueWatchingForActions by remember { mutableStateOf(null) } - var showLibraryListPicker by remember { mutableStateOf(false) } - var pickerItem by remember { mutableStateOf(null) } - var pickerTitle by remember { mutableStateOf("") } - var pickerTabs by remember { mutableStateOf>(emptyList()) } - var pickerMembership by remember { mutableStateOf>(emptyMap()) } - var pickerPending by remember { mutableStateOf(false) } - var pickerError by remember { mutableStateOf(null) } - val addonsUiState by remember { - AddonRepository.initialize() - AddonRepository.uiState - }.collectAsStateWithLifecycle() - val libraryUiState by remember { - LibraryRepository.ensureLoaded() - LibraryRepository.uiState - }.collectAsStateWithLifecycle() - val traktAuthUiState by remember { - TraktAuthRepository.ensureLoaded() - TraktAuthRepository.uiState - }.collectAsStateWithLifecycle() - val authState by AuthRepository.state.collectAsStateWithLifecycle() - val profileState by ProfileRepository.state.collectAsStateWithLifecycle() + val navController = rememberNavController() + val appUpdaterController = rememberAppUpdaterController() + remember { + EpisodeReleaseNotificationsRepository.ensureLoaded() + } + remember { + CollectionSyncService.startObserving() + } + remember { + HomeCatalogSettingsSyncService.startObserving() + } + remember { + ProfileSettingsSync.startObserving() + } + val hapticFeedback = LocalHapticFeedback.current + val coroutineScope = rememberCoroutineScope() + var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) } + var showExitConfirmation by rememberSaveable { mutableStateOf(false) } + var selectedPosterForActions by remember { mutableStateOf(null) } + var selectedContinueWatchingForActions by remember { mutableStateOf(null) } + var showLibraryListPicker by remember { mutableStateOf(false) } + var pickerItem by remember { mutableStateOf(null) } + var pickerTitle by remember { mutableStateOf("") } + var pickerTabs by remember { mutableStateOf>(emptyList()) } + var pickerMembership by remember { mutableStateOf>(emptyMap()) } + var pickerPending by remember { mutableStateOf(false) } + var pickerError by remember { mutableStateOf(null) } + val addonsUiState by remember { + AddonRepository.initialize() + AddonRepository.uiState + }.collectAsStateWithLifecycle() + val libraryUiState by remember { + LibraryRepository.ensureLoaded() + LibraryRepository.uiState + }.collectAsStateWithLifecycle() + val traktAuthUiState by remember { + TraktAuthRepository.ensureLoaded() + TraktAuthRepository.uiState + }.collectAsStateWithLifecycle() + val authState by AuthRepository.state.collectAsStateWithLifecycle() + val profileState by ProfileRepository.state.collectAsStateWithLifecycle() val playerSettingsUiState by remember { PlayerSettingsRepository.ensureLoaded() PlayerSettingsRepository.uiState @@ -500,6 +508,7 @@ private fun MainAppContent( val networkStatusUiState by remember { NetworkStatusRepository.uiState }.collectAsStateWithLifecycle() + val downloadedProviderLabel = stringResource(Res.string.provider_downloaded) val isTraktConnected = traktAuthUiState.mode == TraktConnectionMode.CONNECTED var initialHomeReady by rememberSaveable { mutableStateOf(false) } var offlineLaunchRouteHandled by rememberSaveable { mutableStateOf(false) } @@ -543,11 +552,11 @@ private fun MainAppContent( when (condition) { NetworkCondition.NoInternet -> { - NuvioToastController.show("No internet connection") + NuvioToastController.show(getString(Res.string.network_no_internet_connection)) } NetworkCondition.ServersUnreachable -> { - NuvioToastController.show("Cannot reach servers") + NuvioToastController.show(getString(Res.string.network_cannot_reach_servers)) } NetworkCondition.Online -> { @@ -555,13 +564,13 @@ private fun MainAppContent( previousConditionName == NetworkCondition.NoInternet.name || previousConditionName == NetworkCondition.ServersUnreachable.name ) { - NuvioToastController.show("Back online") + NuvioToastController.show(getString(Res.string.network_back_online)) } } NetworkCondition.Unknown, NetworkCondition.Checking, - -> Unit + -> Unit } lastNetworkToastCondition = condition.name @@ -578,7 +587,7 @@ private fun MainAppContent( when (networkStatusUiState.condition) { NetworkCondition.Unknown, NetworkCondition.Checking, - -> return@LaunchedEffect + -> return@LaunchedEffect NetworkCondition.Online -> { offlineLaunchRouteHandled = true @@ -586,7 +595,7 @@ private fun MainAppContent( NetworkCondition.NoInternet, NetworkCondition.ServersUnreachable, - -> { + -> { offlineLaunchRouteHandled = true val hasPlayableDownload = downloadsUiState.completedItems.any { it.isPlayable } if (hasPlayableDownload) { @@ -628,244 +637,246 @@ private fun MainAppContent( } } - LaunchedEffect(navController) { - AppDeepLinkRepository.pendingDeepLink.collectLatest { deepLink -> - when (deepLink) { - is AppDeepLink.Meta -> { - selectedTab = AppScreenTab.Home - navController.navigate(DetailRoute(type = deepLink.type, id = deepLink.id)) { - launchSingleTop = true - } - AppDeepLinkRepository.markConsumed(deepLink) + LaunchedEffect(navController) { + AppDeepLinkRepository.pendingDeepLink.collectLatest { deepLink -> + when (deepLink) { + is AppDeepLink.Meta -> { + selectedTab = AppScreenTab.Home + navController.navigate(DetailRoute(type = deepLink.type, id = deepLink.id)) { + launchSingleTop = true } + AppDeepLinkRepository.markConsumed(deepLink) + } - AppDeepLink.Downloads -> { - selectedTab = AppScreenTab.Settings - navController.navigate(DownloadsSettingsRoute) { - launchSingleTop = true - } - AppDeepLinkRepository.markConsumed(deepLink) + AppDeepLink.Downloads -> { + selectedTab = AppScreenTab.Settings + navController.navigate(DownloadsSettingsRoute) { + launchSingleTop = true } - - null -> Unit + AppDeepLinkRepository.markConsumed(deepLink) } + + null -> Unit } } + } - fun launchPlaybackWithDownloadPreference( - type: String, - videoId: String, - parentMetaId: String, - parentMetaType: String, - title: String, - logo: String?, - poster: String?, - background: String?, - seasonNumber: Int?, - episodeNumber: Int?, - episodeTitle: String?, - episodeThumbnail: String?, - pauseDescription: String?, - resumePositionMs: Long?, - resumeProgressFraction: Float?, - manualSelection: Boolean, - startFromBeginning: Boolean, - ) { - val targetResumePositionMs = if (startFromBeginning) 0L else (resumePositionMs ?: 0L) - val targetResumeProgressFraction = if (startFromBeginning) null else resumeProgressFraction + fun launchPlaybackWithDownloadPreference( + type: String, + videoId: String, + parentMetaId: String, + parentMetaType: String, + title: String, + logo: String?, + poster: String?, + background: String?, + seasonNumber: Int?, + episodeNumber: Int?, + episodeTitle: String?, + episodeThumbnail: String?, + pauseDescription: String?, + resumePositionMs: Long?, + resumeProgressFraction: Float?, + manualSelection: Boolean, + startFromBeginning: Boolean, + ) { + val targetResumePositionMs = if (startFromBeginning) 0L else (resumePositionMs ?: 0L) + val targetResumeProgressFraction = if (startFromBeginning) null else resumeProgressFraction - if (!manualSelection) { - val downloadedItem = DownloadsRepository.findPlayableDownload( - parentMetaId = parentMetaId, - seasonNumber = seasonNumber, - episodeNumber = episodeNumber, - videoId = videoId, + if (!manualSelection) { + val downloadedItem = DownloadsRepository.findPlayableDownload( + parentMetaId = parentMetaId, + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + videoId = videoId, + ) + val localSourceUrl = downloadedItem?.localFileUri + if (!localSourceUrl.isNullOrBlank()) { + val launchId = PlayerLaunchStore.put( + PlayerLaunch( + title = title, + sourceUrl = localSourceUrl, + sourceHeaders = emptyMap(), + sourceResponseHeaders = emptyMap(), + logo = logo, + poster = poster, + background = background, + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + episodeTitle = episodeTitle, + episodeThumbnail = episodeThumbnail, + streamTitle = downloadedItem.streamTitle.ifBlank { title }, + streamSubtitle = downloadedItem.streamSubtitle, + pauseDescription = pauseDescription, + providerName = downloadedItem.providerName.ifBlank { downloadedProviderLabel }, + providerAddonId = downloadedItem.providerAddonId, + contentType = type, + videoId = videoId, + parentMetaId = parentMetaId, + parentMetaType = parentMetaType, + initialPositionMs = targetResumePositionMs, + initialProgressFraction = targetResumeProgressFraction, + ), ) - val localSourceUrl = downloadedItem?.localFileUri - if (!localSourceUrl.isNullOrBlank()) { - val launchId = PlayerLaunchStore.put( - PlayerLaunch( - title = title, - sourceUrl = localSourceUrl, - sourceHeaders = emptyMap(), - sourceResponseHeaders = emptyMap(), - logo = logo, - poster = poster, - background = background, - seasonNumber = seasonNumber, - episodeNumber = episodeNumber, - episodeTitle = episodeTitle, - episodeThumbnail = episodeThumbnail, - streamTitle = downloadedItem.streamTitle.ifBlank { title }, - streamSubtitle = downloadedItem.streamSubtitle, - pauseDescription = pauseDescription, - providerName = downloadedItem.providerName.ifBlank { "Downloaded" }, - providerAddonId = downloadedItem.providerAddonId, - contentType = type, - videoId = videoId, - parentMetaId = parentMetaId, - parentMetaType = parentMetaType, - initialPositionMs = targetResumePositionMs, - initialProgressFraction = targetResumeProgressFraction, - ), - ) - navController.navigate(PlayerRoute(launchId = launchId)) - return - } + navController.navigate(PlayerRoute(launchId = launchId)) + return } - - val streamLaunchId = StreamLaunchStore.put( - StreamLaunch( - type = type, - videoId = videoId, - parentMetaId = parentMetaId, - parentMetaType = parentMetaType, - title = title, - logo = logo, - poster = poster, - background = background, - seasonNumber = seasonNumber, - episodeNumber = episodeNumber, - episodeTitle = episodeTitle, - episodeThumbnail = episodeThumbnail, - pauseDescription = pauseDescription, - resumePositionMs = if (startFromBeginning) 0L else resumePositionMs, - resumeProgressFraction = targetResumeProgressFraction, - manualSelection = manualSelection, - startFromBeginning = startFromBeginning, - ), - ) - navController.navigate( - StreamRoute(launchId = streamLaunchId), - ) } - val onPlay: (String, String, String, String, String, String?, String?, String?, Int?, Int?, String?, String?, String?, Long?) -> Unit = - { type, videoId, parentMetaId, parentMetaType, title, logo, poster, background, seasonNumber, episodeNumber, episodeTitle, episodeThumbnail, pauseDescription, resumePositionMs -> - launchPlaybackWithDownloadPreference( - type = type, - videoId = videoId, - parentMetaId = parentMetaId, - parentMetaType = parentMetaType, - title = title, - logo = logo, - poster = poster, - background = background, - seasonNumber = seasonNumber, - episodeNumber = episodeNumber, - episodeTitle = episodeTitle, - episodeThumbnail = episodeThumbnail, - pauseDescription = pauseDescription, - resumePositionMs = resumePositionMs, - resumeProgressFraction = null, - manualSelection = false, - startFromBeginning = false, - ) - } - - val onPlayManually: (String, String, String, String, String, String?, String?, String?, Int?, Int?, String?, String?, String?, Long?) -> Unit = - { type, videoId, parentMetaId, parentMetaType, title, logo, poster, background, seasonNumber, episodeNumber, episodeTitle, episodeThumbnail, pauseDescription, resumePositionMs -> - launchPlaybackWithDownloadPreference( - type = type, - videoId = videoId, - parentMetaId = parentMetaId, - parentMetaType = parentMetaType, - title = title, - logo = logo, - poster = poster, - background = background, - seasonNumber = seasonNumber, - episodeNumber = episodeNumber, - episodeTitle = episodeTitle, - episodeThumbnail = episodeThumbnail, - pauseDescription = pauseDescription, - resumePositionMs = resumePositionMs, - resumeProgressFraction = null, - manualSelection = true, - startFromBeginning = false, - ) - } - - val onCatalogClick: (HomeCatalogSection) -> Unit = { section -> - navController.navigate( - CatalogRoute( - title = section.title, - subtitle = section.subtitle, - manifestUrl = section.manifestUrl, - type = section.type, - catalogId = section.catalogId, - supportsPagination = section.supportsPagination, - ), - ) - } - - val onLibrarySectionViewAllClick: (LibrarySection) -> Unit = { section -> - navController.navigate( - CatalogRoute( - title = section.displayTitle, - subtitle = if (libraryUiState.sourceMode == LibrarySourceMode.TRAKT) { - "Trakt Library" - } else { - "Library" - }, - manifestUrl = INTERNAL_LIBRARY_MANIFEST_URL, - type = section.items.firstOrNull()?.type ?: "movie", - catalogId = section.type, - supportsPagination = false, - ), - ) - } - - val openContinueWatching: (ContinueWatchingItem, Boolean, Boolean) -> Unit = { item, manualSelection, startFromBeginning -> - launchPlaybackWithDownloadPreference( - type = item.parentMetaType, - videoId = item.videoId, - parentMetaId = item.parentMetaId, - parentMetaType = item.parentMetaType, - title = item.title, - logo = item.logo, - poster = item.poster, - background = item.background, - seasonNumber = item.seasonNumber, - episodeNumber = item.episodeNumber, - episodeTitle = item.episodeTitle, - episodeThumbnail = item.episodeThumbnail, - pauseDescription = item.pauseDescription, - resumePositionMs = item.resumePositionMs, - resumeProgressFraction = item.resumeProgressFraction, + val streamLaunchId = StreamLaunchStore.put( + StreamLaunch( + type = type, + videoId = videoId, + parentMetaId = parentMetaId, + parentMetaType = parentMetaType, + title = title, + logo = logo, + poster = poster, + background = background, + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + episodeTitle = episodeTitle, + episodeThumbnail = episodeThumbnail, + pauseDescription = pauseDescription, + resumePositionMs = if (startFromBeginning) 0L else resumePositionMs, + resumeProgressFraction = targetResumeProgressFraction, manualSelection = manualSelection, startFromBeginning = startFromBeginning, + ), + ) + navController.navigate( + StreamRoute(launchId = streamLaunchId), + ) + } + + val onPlay: (String, String, String, String, String, String?, String?, String?, Int?, Int?, String?, String?, String?, Long?) -> Unit = + { type, videoId, parentMetaId, parentMetaType, title, logo, poster, background, seasonNumber, episodeNumber, episodeTitle, episodeThumbnail, pauseDescription, resumePositionMs -> + launchPlaybackWithDownloadPreference( + type = type, + videoId = videoId, + parentMetaId = parentMetaId, + parentMetaType = parentMetaType, + title = title, + logo = logo, + poster = poster, + background = background, + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + episodeTitle = episodeTitle, + episodeThumbnail = episodeThumbnail, + pauseDescription = pauseDescription, + resumePositionMs = resumePositionMs, + resumeProgressFraction = null, + manualSelection = false, + startFromBeginning = false, ) } - val onContinueWatchingClick: (ContinueWatchingItem) -> Unit = { item -> - openContinueWatching(item, false, false) + val onPlayManually: (String, String, String, String, String, String?, String?, String?, Int?, Int?, String?, String?, String?, Long?) -> Unit = + { type, videoId, parentMetaId, parentMetaType, title, logo, poster, background, seasonNumber, episodeNumber, episodeTitle, episodeThumbnail, pauseDescription, resumePositionMs -> + launchPlaybackWithDownloadPreference( + type = type, + videoId = videoId, + parentMetaId = parentMetaId, + parentMetaType = parentMetaType, + title = title, + logo = logo, + poster = poster, + background = background, + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + episodeTitle = episodeTitle, + episodeThumbnail = episodeThumbnail, + pauseDescription = pauseDescription, + resumePositionMs = resumePositionMs, + resumeProgressFraction = null, + manualSelection = true, + startFromBeginning = false, + ) } - val onContinueWatchingStartFromBeginning: (ContinueWatchingItem) -> Unit = { item -> - openContinueWatching(item, false, true) - } + val onCatalogClick: (HomeCatalogSection) -> Unit = { section -> + navController.navigate( + CatalogRoute( + title = section.title, + subtitle = section.subtitle, + manifestUrl = section.manifestUrl, + type = section.type, + catalogId = section.catalogId, + supportsPagination = section.supportsPagination, + ), + ) + } - val onContinueWatchingPlayManually: (ContinueWatchingItem) -> Unit = { item -> - openContinueWatching(item, true, false) - } + val librarySectionSubtitle = if (libraryUiState.sourceMode == LibrarySourceMode.TRAKT) { + stringResource(Res.string.compose_catalog_subtitle_trakt_library) + } else { + stringResource(Res.string.compose_catalog_subtitle_library) + } - val onContinueWatchingLongPress: (ContinueWatchingItem) -> Unit = { item -> - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - selectedContinueWatchingForActions = item - } + val onLibrarySectionViewAllClick: (LibrarySection) -> Unit = { section -> + navController.navigate( + CatalogRoute( + title = section.displayTitle, + subtitle = librarySectionSubtitle, + manifestUrl = INTERNAL_LIBRARY_MANIFEST_URL, + type = section.items.firstOrNull()?.type ?: "movie", + catalogId = section.type, + supportsPagination = false, + ), + ) + } - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - ) { - SharedTransitionLayout { - NavHost( - navController = navController, - startDestination = TabsRoute, - modifier = Modifier.fillMaxSize(), - ) { + val openContinueWatching: (ContinueWatchingItem, Boolean, Boolean) -> Unit = { item, manualSelection, startFromBeginning -> + launchPlaybackWithDownloadPreference( + type = item.parentMetaType, + videoId = item.videoId, + parentMetaId = item.parentMetaId, + parentMetaType = item.parentMetaType, + title = item.title, + logo = item.logo, + poster = item.poster, + background = item.background, + seasonNumber = item.seasonNumber, + episodeNumber = item.episodeNumber, + episodeTitle = item.episodeTitle, + episodeThumbnail = item.episodeThumbnail, + pauseDescription = item.pauseDescription, + resumePositionMs = item.resumePositionMs, + resumeProgressFraction = item.resumeProgressFraction, + manualSelection = manualSelection, + startFromBeginning = startFromBeginning, + ) + } + + val onContinueWatchingClick: (ContinueWatchingItem) -> Unit = { item -> + openContinueWatching(item, false, false) + } + + val onContinueWatchingStartFromBeginning: (ContinueWatchingItem) -> Unit = { item -> + openContinueWatching(item, false, true) + } + + val onContinueWatchingPlayManually: (ContinueWatchingItem) -> Unit = { item -> + openContinueWatching(item, true, false) + } + + val onContinueWatchingLongPress: (ContinueWatchingItem) -> Unit = { item -> + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + selectedContinueWatchingForActions = item + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + ) { + SharedTransitionLayout { + NavHost( + navController = navController, + startDestination = TabsRoute, + modifier = Modifier.fillMaxSize(), + ) { composable { PlatformBackHandler( enabled = true, @@ -900,19 +911,19 @@ private fun MainAppContent( selected = selectedTab == AppScreenTab.Home, onClick = { selectedTab = AppScreenTab.Home }, icon = Icons.Filled.Home, - contentDescription = "Home", + contentDescription = stringResource(Res.string.compose_nav_home), ) NavItem( selected = selectedTab == AppScreenTab.Search, onClick = { selectedTab = AppScreenTab.Search }, icon = Res.drawable.sidebar_search, - contentDescription = "Search", + contentDescription = stringResource(Res.string.compose_nav_search), ) NavItem( selected = selectedTab == AppScreenTab.Library, onClick = { selectedTab = AppScreenTab.Library }, icon = Res.drawable.sidebar_library, - contentDescription = "Library", + contentDescription = stringResource(Res.string.compose_nav_library), ) NavItem( selected = selectedTab == AppScreenTab.Settings, @@ -948,7 +959,7 @@ private fun MainAppContent( }, onLibraryPosterLongClick = { item -> hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - selectedPosterForActions = item.toMetaPreview() // reuse existing sheet + selectedPosterForActions = item.toMetaPreview() }, onLibrarySectionViewAllClick = onLibrarySectionViewAllClick, onContinueWatchingClick = onContinueWatchingClick, @@ -999,6 +1010,9 @@ private fun MainAppContent( } composable { backStackEntry -> val route = backStackEntry.toRoute() + val directorRole = stringResource(Res.string.person_role_director) + val writerRole = stringResource(Res.string.person_role_writer) + val creatorRole = stringResource(Res.string.person_role_creator) MetaDetailsScreen( type = route.type, id = route.id, @@ -1039,8 +1053,11 @@ private fun MainAppContent( castAvatarTransitionKey = avatarTransitionKey, preferCrew = person.role?.let { it.equals("Director", ignoreCase = true) || - it.equals("Writer", ignoreCase = true) || - it.equals("Creator", ignoreCase = true) + it.equals(directorRole, ignoreCase = true) || + it.equals("Writer", ignoreCase = true) || + it.equals(writerRole, ignoreCase = true) || + it.equals("Creator", ignoreCase = true) + || it.equals(creatorRole, ignoreCase = true) } ?: false, ), ) @@ -1158,8 +1175,8 @@ private fun MainAppContent( } val shouldResolveEpisodeVideoId = launch.parentMetaId != null && - launch.seasonNumber != null && - launch.episodeNumber != null + launch.seasonNumber != null && + launch.episodeNumber != null var effectiveVideoId by rememberSaveable( launch.videoId, launch.parentMetaId, @@ -1200,7 +1217,7 @@ private fun MainAppContent( ?.videos ?.firstOrNull { video -> video.season == launch.seasonNumber && - video.episode == launch.episodeNumber + video.episode == launch.episodeNumber } ?.id ?.takeIf { it.isNotBlank() } @@ -1630,199 +1647,199 @@ private fun MainAppContent( }, ) } - } } + } - NuvioPosterActionSheet( - item = selectedPosterForActions, - isSaved = selectedPosterForActions?.let { preview -> - LibraryRepository.isSaved(preview.id, preview.type) - } == true, - isWatched = selectedPosterForActions?.let { preview -> - WatchingState.isPosterWatched( - watchedKeys = watchedUiState.watchedKeys, - item = preview, - ) - } == true, - onDismiss = { selectedPosterForActions = null }, - onToggleLibrary = { - selectedPosterForActions?.let { preview -> - val libraryItem = preview.toLibraryItem(savedAtEpochMs = 0L) - if (!isTraktConnected) { - LibraryRepository.toggleSaved(libraryItem) - } else { - pickerItem = libraryItem - pickerTitle = preview.name - pickerTabs = LibraryRepository.traktListTabs() - pickerMembership = pickerTabs.associate { it.key to false } - pickerPending = true - pickerError = null - showLibraryListPicker = true - coroutineScope.launch { - runCatching { - val snapshot = LibraryRepository.getMembershipSnapshot(libraryItem) - val tabs = LibraryRepository.traktListTabs() - pickerTabs = tabs - pickerMembership = tabs.associate { tab -> - tab.key to (snapshot[tab.key] == true) - } - }.onFailure { error -> - pickerError = error.message ?: "Failed to load Trakt lists" - } - pickerPending = false - } - } - } - }, - onToggleWatched = { - selectedPosterForActions?.let { preview -> + NuvioPosterActionSheet( + item = selectedPosterForActions, + isSaved = selectedPosterForActions?.let { preview -> + LibraryRepository.isSaved(preview.id, preview.type) + } == true, + isWatched = selectedPosterForActions?.let { preview -> + WatchingState.isPosterWatched( + watchedKeys = watchedUiState.watchedKeys, + item = preview, + ) + } == true, + onDismiss = { selectedPosterForActions = null }, + onToggleLibrary = { + selectedPosterForActions?.let { preview -> + val libraryItem = preview.toLibraryItem(savedAtEpochMs = 0L) + if (!isTraktConnected) { + LibraryRepository.toggleSaved(libraryItem) + } else { + pickerItem = libraryItem + pickerTitle = preview.name + pickerTabs = LibraryRepository.traktListTabs() + pickerMembership = pickerTabs.associate { it.key to false } + pickerPending = true + pickerError = null + showLibraryListPicker = true coroutineScope.launch { - WatchingActions.togglePosterWatched(preview) + runCatching { + val snapshot = LibraryRepository.getMembershipSnapshot(libraryItem) + val tabs = LibraryRepository.traktListTabs() + pickerTabs = tabs + pickerMembership = tabs.associate { tab -> + tab.key to (snapshot[tab.key] == true) + } + }.onFailure { error -> + pickerError = error.message ?: getString(Res.string.trakt_lists_load_failed) + } + pickerPending = false } } - }, - ) + } + }, + onToggleWatched = { + selectedPosterForActions?.let { preview -> + coroutineScope.launch { + WatchingActions.togglePosterWatched(preview) + } + } + }, + ) - NuvioContinueWatchingActionSheet( - item = selectedContinueWatchingForActions, - showManualPlayOption = StreamAutoPlayPolicy.isEffectivelyEnabled(playerSettingsUiState), - onDismiss = { selectedContinueWatchingForActions = null }, - onOpenDetails = { - selectedContinueWatchingForActions?.let { item -> - navController.navigate( - DetailRoute( - type = item.parentMetaType, - id = item.parentMetaId, + NuvioContinueWatchingActionSheet( + item = selectedContinueWatchingForActions, + showManualPlayOption = StreamAutoPlayPolicy.isEffectivelyEnabled(playerSettingsUiState), + onDismiss = { selectedContinueWatchingForActions = null }, + onOpenDetails = { + selectedContinueWatchingForActions?.let { item -> + navController.navigate( + DetailRoute( + type = item.parentMetaType, + id = item.parentMetaId, + ), + ) + } + }, + onStartFromBeginning = selectedContinueWatchingForActions + ?.takeIf { !it.isNextUp } + ?.let { item -> { onContinueWatchingStartFromBeginning(item) } }, + onPlayManually = selectedContinueWatchingForActions + ?.let { item -> { onContinueWatchingPlayManually(item) } }, + onRemove = { + selectedContinueWatchingForActions?.let { item -> + if (item.isNextUp) { + ContinueWatchingPreferencesRepository.addDismissedNextUpKey( + nextUpDismissKey( + item.parentMetaId, + item.nextUpSeedSeasonNumber, + item.nextUpSeedEpisodeNumber, ), ) + } else { + WatchProgressRepository.removeProgress(contentId = item.parentMetaId) } - }, - onStartFromBeginning = selectedContinueWatchingForActions - ?.takeIf { !it.isNextUp } - ?.let { item -> { onContinueWatchingStartFromBeginning(item) } }, - onPlayManually = selectedContinueWatchingForActions - ?.let { item -> { onContinueWatchingPlayManually(item) } }, - onRemove = { - selectedContinueWatchingForActions?.let { item -> - if (item.isNextUp) { - ContinueWatchingPreferencesRepository.addDismissedNextUpKey( - nextUpDismissKey( - item.parentMetaId, - item.nextUpSeedSeasonNumber, - item.nextUpSeedEpisodeNumber, - ), - ) - } else { - WatchProgressRepository.removeProgress(contentId = item.parentMetaId) - } - } - }, - ) + } + }, + ) - TraktListPickerDialog( - visible = showLibraryListPicker, - title = pickerTitle, - tabs = pickerTabs, - membership = pickerMembership, - isPending = pickerPending, - errorMessage = pickerError, - onToggle = { listKey -> - pickerMembership = pickerMembership.toMutableMap().apply { - this[listKey] = !(this[listKey] == true) - } - }, - onDismiss = { - if (!pickerPending) { + TraktListPickerDialog( + visible = showLibraryListPicker, + title = pickerTitle, + tabs = pickerTabs, + membership = pickerMembership, + isPending = pickerPending, + errorMessage = pickerError, + onToggle = { listKey -> + pickerMembership = pickerMembership.toMutableMap().apply { + this[listKey] = !(this[listKey] == true) + } + }, + onDismiss = { + if (!pickerPending) { + showLibraryListPicker = false + pickerItem = null + pickerError = null + } + }, + onSave = { + val item = pickerItem ?: return@TraktListPickerDialog + coroutineScope.launch { + pickerPending = true + pickerError = null + runCatching { + LibraryRepository.applyMembershipChanges( + item = item, + desiredMembership = pickerMembership, + ) + }.onSuccess { showLibraryListPicker = false pickerItem = null pickerError = null + }.onFailure { error -> + pickerError = error.message ?: getString(Res.string.trakt_lists_update_failed) } - }, - onSave = { - val item = pickerItem ?: return@TraktListPickerDialog - coroutineScope.launch { - pickerPending = true - pickerError = null - runCatching { - LibraryRepository.applyMembershipChanges( - item = item, - desiredMembership = pickerMembership, - ) - }.onSuccess { - showLibraryListPicker = false - pickerItem = null - pickerError = null - }.onFailure { error -> - pickerError = error.message ?: "Failed to update Trakt lists" - } - pickerPending = false - } - }, - ) - - NuvioStatusModal( - title = "Exit app", - message = "Do you want to exit the app?", - isVisible = showExitConfirmation, - confirmText = "Yes", - dismissText = "No", - onConfirm = { - showExitConfirmation = false - platformExitApp() - }, - onDismiss = { - showExitConfirmation = false - }, - ) - - androidx.compose.animation.AnimatedVisibility( - visible = !initialHomeReady || profileSwitchLoading, - enter = fadeIn(), - exit = fadeOut(androidx.compose.animation.core.tween(400)), - ) { - AppLaunchOverlay(modifier = Modifier.fillMaxSize()) - } - - // Auto-dismiss profile switch overlay - if (profileSwitchLoading) { - LaunchedEffect(Unit) { - // Brief loading screen while home refreshes for the new profile - kotlinx.coroutines.delay(1200) - profileSwitchLoading = false + pickerPending = false } - } + }, + ) - NuvioFloatingPrompt( - visible = resumePromptItem != null, - imageUrl = resumePromptItem?.poster ?: resumePromptItem?.imageUrl, - title = resumePromptItem?.title.orEmpty(), - subtitle = resumePromptItem?.subtitle.orEmpty(), - progressFraction = resumePromptItem?.progressFraction ?: 0f, - actionLabel = "Resume", - onAction = { - val item = resumePromptItem ?: return@NuvioFloatingPrompt - resumePromptItem = null - openContinueWatching(item, false, false) - }, - onDismiss = { resumePromptItem = null }, - modifier = Modifier - .align(Alignment.BottomCenter) - .zIndex(15f), - ) + NuvioStatusModal( + title = stringResource(Res.string.app_exit_title), + message = stringResource(Res.string.app_exit_message), + isVisible = showExitConfirmation, + confirmText = stringResource(Res.string.action_yes), + dismissText = stringResource(Res.string.action_no), + onConfirm = { + showExitConfirmation = false + platformExitApp() + }, + onDismiss = { + showExitConfirmation = false + }, + ) - NuvioToastHost( - modifier = Modifier - .align(Alignment.TopCenter) - .zIndex(20f), - ) - - AppUpdaterHost( - controller = appUpdaterController, - modifier = Modifier - .align(Alignment.Center) - .zIndex(25f), - ) + androidx.compose.animation.AnimatedVisibility( + visible = !initialHomeReady || profileSwitchLoading, + enter = fadeIn(), + exit = fadeOut(androidx.compose.animation.core.tween(400)), + ) { + AppLaunchOverlay(modifier = Modifier.fillMaxSize()) } + + // Auto-dismiss profile switch overlay + if (profileSwitchLoading) { + LaunchedEffect(Unit) { + // Brief loading screen while home refreshes for the new profile + kotlinx.coroutines.delay(1200) + profileSwitchLoading = false + } + } + + NuvioFloatingPrompt( + visible = resumePromptItem != null, + imageUrl = resumePromptItem?.poster ?: resumePromptItem?.imageUrl, + title = resumePromptItem?.title.orEmpty(), + subtitle = resumePromptItem?.let { localizedContinueWatchingSubtitle(it) }.orEmpty(), + progressFraction = resumePromptItem?.progressFraction ?: 0f, + actionLabel = stringResource(Res.string.resume_prompt_action), + onAction = { + val item = resumePromptItem ?: return@NuvioFloatingPrompt + resumePromptItem = null + openContinueWatching(item, false, false) + }, + onDismiss = { resumePromptItem = null }, + modifier = Modifier + .align(Alignment.BottomCenter) + .zIndex(15f), + ) + + NuvioToastHost( + modifier = Modifier + .align(Alignment.TopCenter) + .zIndex(20f), + ) + + AppUpdaterHost( + controller = appUpdaterController, + modifier = Modifier + .align(Alignment.Center) + .zIndex(25f), + ) + } } @Composable @@ -1853,6 +1870,7 @@ private fun AppTabHost( onPosterClick: ((MetaPreview) -> Unit)? = null, onPosterLongClick: ((MetaPreview) -> Unit)? = null, onLibraryPosterClick: ((LibraryItem) -> Unit)? = null, + onLibraryPosterLongClick: ((LibraryItem) -> Unit)? = null, onLibrarySectionViewAllClick: ((LibrarySection) -> Unit)? = null, onContinueWatchingClick: ((ContinueWatchingItem) -> Unit)? = null, onContinueWatchingLongPress: ((ContinueWatchingItem) -> Unit)? = null, @@ -1869,7 +1887,6 @@ private fun AppTabHost( onCollectionsSettingsClick: () -> Unit = {}, onFolderClick: ((collectionId: String, folderId: String) -> Unit)? = null, onInitialHomeContentRendered: () -> Unit = {}, - onLibraryPosterLongClick: ((LibraryItem) -> Unit)? = null, ) { val tabStateHolder = rememberSaveableStateHolder() @@ -1955,13 +1972,13 @@ private fun TabletFloatingTopBar( verticalAlignment = Alignment.CenterVertically, ) { TabletTopPillItem( - label = "Home", + label = stringResource(Res.string.compose_nav_home), selected = selectedTab == AppScreenTab.Home, onClick = { onTabSelected(AppScreenTab.Home) }, icon = { Icon( imageVector = Icons.Filled.Home, - contentDescription = "Home", + contentDescription = stringResource(Res.string.compose_nav_home), modifier = Modifier.size(18.dp), tint = if (selectedTab == AppScreenTab.Home) { MaterialTheme.colorScheme.onPrimaryContainer @@ -1972,13 +1989,13 @@ private fun TabletFloatingTopBar( }, ) TabletTopPillItem( - label = "Search", + label = stringResource(Res.string.compose_nav_search), selected = selectedTab == AppScreenTab.Search, onClick = { onTabSelected(AppScreenTab.Search) }, icon = { Icon( painter = painterResource(Res.drawable.sidebar_search), - contentDescription = "Search", + contentDescription = stringResource(Res.string.compose_nav_search), modifier = Modifier.size(18.dp), tint = if (selectedTab == AppScreenTab.Search) { MaterialTheme.colorScheme.onPrimaryContainer @@ -1989,13 +2006,13 @@ private fun TabletFloatingTopBar( }, ) TabletTopPillItem( - label = "Library", + label = stringResource(Res.string.compose_nav_library), selected = selectedTab == AppScreenTab.Library, onClick = { onTabSelected(AppScreenTab.Library) }, icon = { Icon( painter = painterResource(Res.drawable.sidebar_library), - contentDescription = "Library", + contentDescription = stringResource(Res.string.compose_nav_library), modifier = Modifier.size(18.dp), tint = if (selectedTab == AppScreenTab.Library) { MaterialTheme.colorScheme.onPrimaryContainer @@ -2025,7 +2042,7 @@ private fun TabletFloatingTopBar( onAddProfileRequested = onAddProfileRequested, ) Text( - text = "Profile", + text = stringResource(Res.string.compose_nav_profile), modifier = Modifier.clickable { onTabSelected(AppScreenTab.Settings) }, style = MaterialTheme.typography.labelLarge, color = if (selectedTab == AppScreenTab.Settings) { @@ -2088,7 +2105,7 @@ private fun AppLaunchOverlay( ) { Image( painter = painterResource(Res.drawable.app_logo_wordmark), - contentDescription = "Nuvio", + contentDescription = stringResource(Res.string.app_brand_name), modifier = Modifier .fillMaxWidth(0.48f) .height(44.dp), From c29f0cc06416718944e6908014027442af72a377 Mon Sep 17 00:00:00 2001 From: AdityasahuX07 Date: Tue, 12 May 2026 23:01:29 +0530 Subject: [PATCH 4/6] Refactor LibraryScreen to use action targets --- .../app/features/library/LibraryScreen.kt | 115 ++++++++---------- 1 file changed, 49 insertions(+), 66 deletions(-) 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 8c94c676..85623512 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 @@ -21,10 +21,10 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nuvio.app.core.network.NetworkCondition import com.nuvio.app.core.network.NetworkStatusRepository +import com.nuvio.app.core.ui.NuvioPosterActionSheet import com.nuvio.app.core.ui.NuvioScreen import com.nuvio.app.core.ui.NuvioNetworkOfflineCard import com.nuvio.app.core.ui.NuvioScreenHeader -import com.nuvio.app.core.ui.NuvioStatusModal import com.nuvio.app.core.ui.NuvioToastController import com.nuvio.app.core.ui.NuvioViewAllPillSize import com.nuvio.app.core.ui.NuvioShelfSection @@ -32,22 +32,20 @@ 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.WatchingActions +import com.nuvio.app.features.watching.application.WatchingState import kotlinx.coroutines.launch -import nuvio.composeapp.generated.resources.* -import org.jetbrains.compose.resources.getString -import org.jetbrains.compose.resources.stringResource -private data class LibraryRemovalTarget( +private data class LibraryActionTarget( val item: LibraryItem, - val listKey: String? = null, - val listTitle: String? = null, + val section: LibrarySection, ) @Composable fun LibraryScreen( modifier: Modifier = Modifier, onPosterClick: ((LibraryItem) -> Unit)? = null, - onPosterLongClick: ((LibraryItem) -> Unit)? = null, onSectionViewAllClick: ((LibrarySection) -> Unit)? = null, ) { val uiState by remember { @@ -55,7 +53,8 @@ fun LibraryScreen( LibraryRepository.uiState }.collectAsStateWithLifecycle() val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle() - var pendingRemovalTarget by remember { mutableStateOf(null) } + val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle() + var actionTarget by remember { mutableStateOf(null) } var observedOfflineState by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() val isTraktSource = uiState.sourceMode == LibrarySourceMode.TRAKT @@ -66,12 +65,11 @@ fun LibraryScreen( } } - LaunchedEffect(networkStatusUiState.condition, isTraktSource) { when (networkStatusUiState.condition) { NetworkCondition.NoInternet, NetworkCondition.ServersUnreachable, - -> { + -> { observedOfflineState = true } @@ -87,7 +85,7 @@ fun LibraryScreen( NetworkCondition.Unknown, NetworkCondition.Checking, - -> Unit + -> Unit } } @@ -102,11 +100,7 @@ fun LibraryScreen( .background(MaterialTheme.colorScheme.background), ) { NuvioScreenHeader( - title = if (isTraktSource) { - stringResource(Res.string.library_trakt_title) - } else { - stringResource(Res.string.library_title) - }, + title = if (isTraktSource) "Trakt Library" else "Library", modifier = Modifier.padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(6.dp)) @@ -131,13 +125,9 @@ fun LibraryScreen( } else { HomeEmptyStateCard( modifier = Modifier.padding(horizontal = 16.dp), - title = if (isTraktSource) { - stringResource(Res.string.library_trakt_load_failed) - } else { - stringResource(Res.string.library_load_failed) - }, + title = if (isTraktSource) "Couldn't load Trakt library" else "Couldn't load library", message = uiState.errorMessage.orEmpty(), - actionLabel = stringResource(Res.string.action_retry), + actionLabel = "Retry", onActionClick = retryLibraryLoad, ) } @@ -155,15 +145,11 @@ fun LibraryScreen( } else { HomeEmptyStateCard( modifier = Modifier.padding(horizontal = 16.dp), - title = if (isTraktSource) { - stringResource(Res.string.library_trakt_empty_title) - } else { - stringResource(Res.string.library_empty_title) - }, + title = if (isTraktSource) "Your Trakt library is empty" else "Your library is empty", message = if (isTraktSource) { - stringResource(Res.string.library_trakt_empty_message) + "Connect Trakt and save titles to your watchlist or personal lists." } else { - stringResource(Res.string.library_empty_message) + "Saved titles will appear here after you tap Save on a details screen." }, ) } @@ -176,56 +162,53 @@ fun LibraryScreen( onPosterClick = onPosterClick, onSectionViewAllClick = onSectionViewAllClick, onPosterLongClick = { item, section -> - pendingRemovalTarget = if (isTraktSource) { - LibraryRemovalTarget( - item = item, - listKey = section.type, - listTitle = section.displayTitle, - ) - } else { - LibraryRemovalTarget(item = item) - } + actionTarget = LibraryActionTarget(item = item, section = section) }, ) } } } - NuvioStatusModal( - title = stringResource(Res.string.library_remove_title), - message = pendingRemovalTarget?.let { target -> - val listTitle = target.listTitle - if (listTitle.isNullOrBlank()) { - stringResource(Res.string.library_remove_message, target.item.name) - } else { - stringResource(Res.string.library_remove_from_list_message, target.item.name, listTitle) - } - }.orEmpty(), - isVisible = pendingRemovalTarget != null, - confirmText = stringResource(Res.string.library_remove_confirm), - dismissText = stringResource(Res.string.action_cancel), - onConfirm = { - val target = pendingRemovalTarget - pendingRemovalTarget = null - target?.let { - val listKey = target.listKey - if (listKey.isNullOrBlank()) { - LibraryRepository.remove(target.item.id) - } else { + // Action sheet — same UI as home screen poster long-press + val target = actionTarget + if (target != null) { + val preview = target.item.toMetaPreview() + NuvioPosterActionSheet( + item = preview, + isSaved = LibraryRepository.isSaved(preview.id, preview.type), + isWatched = WatchingState.isPosterWatched( + watchedKeys = watchedUiState.watchedKeys, + item = preview, + ), + onDismiss = { actionTarget = null }, + onToggleLibrary = { + actionTarget = null + val libraryItem = target.item + if (isTraktSource) { coroutineScope.launch { runCatching { - LibraryRepository.removeFromList(target.item, listKey) + LibraryRepository.removeFromList( + libraryItem, + target.section.type, + ) }.onFailure { error -> NuvioToastController.show( - error.message ?: getString(Res.string.trakt_lists_update_failed), + error.message ?: "Failed to update Trakt list", ) } } + } else { + LibraryRepository.toggleSaved(libraryItem) } - } - }, - onDismiss = { pendingRemovalTarget = null }, - ) + }, + onToggleWatched = { + actionTarget = null + coroutineScope.launch { + WatchingActions.togglePosterWatched(preview) + } + }, + ) + } } private fun LazyListScope.librarySections( From 753f19d5818fa7e15648c6188e0e8fcc54098180 Mon Sep 17 00:00:00 2001 From: AdityasahuX07 Date: Tue, 12 May 2026 23:03:49 +0530 Subject: [PATCH 5/6] Update App.kt --- .../commonMain/kotlin/com/nuvio/app/App.kt | 517 ++++++++---------- 1 file changed, 232 insertions(+), 285 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 140ea86e..2d1fbaad 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -123,7 +123,6 @@ import com.nuvio.app.features.library.LibrarySection import com.nuvio.app.features.library.LibrarySourceMode import com.nuvio.app.features.library.LibraryScreen import com.nuvio.app.features.library.toLibraryItem -import com.nuvio.app.features.library.toMetaPreview import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepository import com.nuvio.app.features.player.PlayerLaunch import com.nuvio.app.features.player.PlayerLaunchStore @@ -385,8 +384,8 @@ fun App() { val cachedProfiles = profileState.profiles val allowOfflineProfileAccess = cachedProfiles.isNotEmpty() && - authState !is AuthState.Authenticated && - networkStatusUiState.condition != NetworkCondition.Online + authState !is AuthState.Authenticated && + networkStatusUiState.condition != NetworkCondition.Online when (authState) { is AuthState.Loading -> { @@ -660,7 +659,7 @@ private fun MainAppContent( NetworkCondition.Unknown, NetworkCondition.Checking, - -> Unit + -> Unit } lastNetworkToastCondition = condition.name @@ -677,7 +676,7 @@ private fun MainAppContent( when (networkStatusUiState.condition) { NetworkCondition.Unknown, NetworkCondition.Checking, - -> return@LaunchedEffect + -> return@LaunchedEffect NetworkCondition.Online -> { offlineLaunchRouteHandled = true @@ -685,7 +684,7 @@ private fun MainAppContent( NetworkCondition.NoInternet, NetworkCondition.ServersUnreachable, - -> { + -> { offlineLaunchRouteHandled = true val hasPlayableDownload = downloadsUiState.completedItems.any { DownloadsRepository.playableLocalFileUri(it) != null @@ -729,24 +728,29 @@ private fun MainAppContent( } } - LaunchedEffect(navController) { - AppDeepLinkRepository.pendingDeepLink.collectLatest { deepLink -> - when (deepLink) { - is AppDeepLink.Meta -> { - selectedTab = AppScreenTab.Home - navController.navigate(DetailRoute(type = deepLink.type, id = deepLink.id)) { - launchSingleTop = true + LaunchedEffect(navController) { + AppDeepLinkRepository.pendingDeepLink.collectLatest { deepLink -> + when (deepLink) { + is AppDeepLink.Meta -> { + selectedTab = AppScreenTab.Home + navController.navigate(DetailRoute(type = deepLink.type, id = deepLink.id)) { + launchSingleTop = true + } + AppDeepLinkRepository.markConsumed(deepLink) } - AppDeepLinkRepository.markConsumed(deepLink) - } - AppDeepLink.Downloads -> { - selectedTab = AppScreenTab.Settings - navController.navigate(DownloadsSettingsRoute) { - launchSingleTop = true + AppDeepLink.Downloads -> { + selectedTab = AppScreenTab.Settings + navController.navigate(DownloadsSettingsRoute) { + launchSingleTop = true + } + AppDeepLinkRepository.markConsumed(deepLink) } - AppDeepLinkRepository.markConsumed(deepLink) + + null -> Unit } + } + } fun launchPlaybackWithDownloadPreference( type: String, @@ -809,69 +813,90 @@ private fun MainAppContent( return } } - } - } - fun launchPlaybackWithDownloadPreference( - type: String, - videoId: String, - parentMetaId: String, - parentMetaType: String, - title: String, - logo: String?, - poster: String?, - background: String?, - seasonNumber: Int?, - episodeNumber: Int?, - episodeTitle: String?, - episodeThumbnail: String?, - pauseDescription: String?, - resumePositionMs: Long?, - resumeProgressFraction: Float?, - manualSelection: Boolean, - startFromBeginning: Boolean, - ) { - val targetResumePositionMs = if (startFromBeginning) 0L else (resumePositionMs ?: 0L) - val targetResumeProgressFraction = if (startFromBeginning) null else resumeProgressFraction - - if (!manualSelection) { - val downloadedItem = DownloadsRepository.findPlayableDownload( - parentMetaId = parentMetaId, - seasonNumber = seasonNumber, - episodeNumber = episodeNumber, - videoId = videoId, + val streamLaunchId = StreamLaunchStore.put( + StreamLaunch( + type = type, + videoId = videoId, + parentMetaId = parentMetaId, + parentMetaType = parentMetaType, + title = title, + logo = logo, + poster = poster, + background = background, + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + episodeTitle = episodeTitle, + episodeThumbnail = episodeThumbnail, + pauseDescription = pauseDescription, + resumePositionMs = if (startFromBeginning) 0L else resumePositionMs, + resumeProgressFraction = targetResumeProgressFraction, + manualSelection = manualSelection, + startFromBeginning = startFromBeginning, + ), ) - val localSourceUrl = downloadedItem?.localFileUri - if (!localSourceUrl.isNullOrBlank()) { - val launchId = PlayerLaunchStore.put( - PlayerLaunch( - title = title, - sourceUrl = localSourceUrl, - sourceHeaders = emptyMap(), - sourceResponseHeaders = emptyMap(), - logo = logo, - poster = poster, - background = background, - seasonNumber = seasonNumber, - episodeNumber = episodeNumber, - episodeTitle = episodeTitle, - episodeThumbnail = episodeThumbnail, - streamTitle = downloadedItem.streamTitle.ifBlank { title }, - streamSubtitle = downloadedItem.streamSubtitle, - pauseDescription = pauseDescription, - providerName = downloadedItem.providerName.ifBlank { downloadedProviderLabel }, - providerAddonId = downloadedItem.providerAddonId, - contentType = type, - videoId = videoId, - parentMetaId = parentMetaId, - parentMetaType = parentMetaType, - initialPositionMs = targetResumePositionMs, - initialProgressFraction = targetResumeProgressFraction, - ), + navController.navigate( + StreamRoute(launchId = streamLaunchId), + ) + } + + val onPlay: (String, String, String, String, String, String?, String?, String?, Int?, Int?, String?, String?, String?, Long?) -> Unit = + { type, videoId, parentMetaId, parentMetaType, title, logo, poster, background, seasonNumber, episodeNumber, episodeTitle, episodeThumbnail, pauseDescription, resumePositionMs -> + launchPlaybackWithDownloadPreference( + type = type, + videoId = videoId, + parentMetaId = parentMetaId, + parentMetaType = parentMetaType, + title = title, + logo = logo, + poster = poster, + background = background, + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + episodeTitle = episodeTitle, + episodeThumbnail = episodeThumbnail, + pauseDescription = pauseDescription, + resumePositionMs = resumePositionMs, + resumeProgressFraction = null, + manualSelection = false, + startFromBeginning = false, ) - navController.navigate(PlayerRoute(launchId = launchId)) - return } + + val onPlayManually: (String, String, String, String, String, String?, String?, String?, Int?, Int?, String?, String?, String?, Long?) -> Unit = + { type, videoId, parentMetaId, parentMetaType, title, logo, poster, background, seasonNumber, episodeNumber, episodeTitle, episodeThumbnail, pauseDescription, resumePositionMs -> + launchPlaybackWithDownloadPreference( + type = type, + videoId = videoId, + parentMetaId = parentMetaId, + parentMetaType = parentMetaType, + title = title, + logo = logo, + poster = poster, + background = background, + seasonNumber = seasonNumber, + episodeNumber = episodeNumber, + episodeTitle = episodeTitle, + episodeThumbnail = episodeThumbnail, + pauseDescription = pauseDescription, + resumePositionMs = resumePositionMs, + resumeProgressFraction = null, + manualSelection = true, + startFromBeginning = false, + ) + } + + val onCatalogClick: (HomeCatalogSection) -> Unit = { section -> + navController.navigate( + CatalogRoute( + title = section.title, + subtitle = section.subtitle, + manifestUrl = section.manifestUrl, + type = section.type, + catalogId = section.catalogId, + supportsPagination = section.supportsPagination, + ), + ) } val librarySectionSubtitle = if (libraryUiState.sourceMode == LibrarySourceMode.TRAKT) { @@ -893,111 +918,56 @@ private fun MainAppContent( ) } - val onPlayManually: (String, String, String, String, String, String?, String?, String?, Int?, Int?, String?, String?, String?, Long?) -> Unit = - { type, videoId, parentMetaId, parentMetaType, title, logo, poster, background, seasonNumber, episodeNumber, episodeTitle, episodeThumbnail, pauseDescription, resumePositionMs -> + val openContinueWatching: (ContinueWatchingItem, Boolean, Boolean) -> Unit = { item, manualSelection, startFromBeginning -> launchPlaybackWithDownloadPreference( - type = type, - videoId = videoId, - parentMetaId = parentMetaId, - parentMetaType = parentMetaType, - title = title, - logo = logo, - poster = poster, - background = background, - seasonNumber = seasonNumber, - episodeNumber = episodeNumber, - episodeTitle = episodeTitle, - episodeThumbnail = episodeThumbnail, - pauseDescription = pauseDescription, - resumePositionMs = resumePositionMs, - resumeProgressFraction = null, - manualSelection = true, - startFromBeginning = false, + type = item.parentMetaType, + videoId = item.videoId, + parentMetaId = item.parentMetaId, + parentMetaType = item.parentMetaType, + title = item.title, + logo = item.logo, + poster = item.poster, + background = item.background, + seasonNumber = item.seasonNumber, + episodeNumber = item.episodeNumber, + episodeTitle = item.episodeTitle, + episodeThumbnail = item.episodeThumbnail, + pauseDescription = item.pauseDescription, + resumePositionMs = item.resumePositionMs, + resumeProgressFraction = item.resumeProgressFraction, + manualSelection = manualSelection, + startFromBeginning = startFromBeginning, ) } - val onCatalogClick: (HomeCatalogSection) -> Unit = { section -> - navController.navigate( - CatalogRoute( - title = section.title, - subtitle = section.subtitle, - manifestUrl = section.manifestUrl, - type = section.type, - catalogId = section.catalogId, - supportsPagination = section.supportsPagination, - ), - ) - } + val onContinueWatchingClick: (ContinueWatchingItem) -> Unit = { item -> + openContinueWatching(item, false, false) + } - val librarySectionSubtitle = if (libraryUiState.sourceMode == LibrarySourceMode.TRAKT) { - stringResource(Res.string.compose_catalog_subtitle_trakt_library) - } else { - stringResource(Res.string.compose_catalog_subtitle_library) - } + val onContinueWatchingStartFromBeginning: (ContinueWatchingItem) -> Unit = { item -> + openContinueWatching(item, false, true) + } - val onLibrarySectionViewAllClick: (LibrarySection) -> Unit = { section -> - navController.navigate( - CatalogRoute( - title = section.displayTitle, - subtitle = librarySectionSubtitle, - manifestUrl = INTERNAL_LIBRARY_MANIFEST_URL, - type = section.items.firstOrNull()?.type ?: "movie", - catalogId = section.type, - supportsPagination = false, - ), - ) - } + val onContinueWatchingPlayManually: (ContinueWatchingItem) -> Unit = { item -> + openContinueWatching(item, true, false) + } - val openContinueWatching: (ContinueWatchingItem, Boolean, Boolean) -> Unit = { item, manualSelection, startFromBeginning -> - launchPlaybackWithDownloadPreference( - type = item.parentMetaType, - videoId = item.videoId, - parentMetaId = item.parentMetaId, - parentMetaType = item.parentMetaType, - title = item.title, - logo = item.logo, - poster = item.poster, - background = item.background, - seasonNumber = item.seasonNumber, - episodeNumber = item.episodeNumber, - episodeTitle = item.episodeTitle, - episodeThumbnail = item.episodeThumbnail, - pauseDescription = item.pauseDescription, - resumePositionMs = item.resumePositionMs, - resumeProgressFraction = item.resumeProgressFraction, - manualSelection = manualSelection, - startFromBeginning = startFromBeginning, - ) - } + val onContinueWatchingLongPress: (ContinueWatchingItem) -> Unit = { item -> + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + selectedContinueWatchingForActions = item + } - val onContinueWatchingClick: (ContinueWatchingItem) -> Unit = { item -> - openContinueWatching(item, false, false) - } - - val onContinueWatchingStartFromBeginning: (ContinueWatchingItem) -> Unit = { item -> - openContinueWatching(item, false, true) - } - - val onContinueWatchingPlayManually: (ContinueWatchingItem) -> Unit = { item -> - openContinueWatching(item, true, false) - } - - val onContinueWatchingLongPress: (ContinueWatchingItem) -> Unit = { item -> - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - selectedContinueWatchingForActions = item - } - - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - ) { - SharedTransitionLayout { - NavHost( - navController = navController, - startDestination = TabsRoute, - modifier = Modifier.fillMaxSize(), - ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + ) { + SharedTransitionLayout { + NavHost( + navController = navController, + startDestination = TabsRoute, + modifier = Modifier.fillMaxSize(), + ) { composable { PlatformBackHandler( enabled = true, @@ -1303,8 +1273,8 @@ private fun MainAppContent( } val shouldResolveEpisodeVideoId = launch.parentMetaId != null && - launch.seasonNumber != null && - launch.episodeNumber != null + launch.seasonNumber != null && + launch.episodeNumber != null var effectiveVideoId by rememberSaveable( launch.videoId, launch.parentMetaId, @@ -1345,7 +1315,7 @@ private fun MainAppContent( ?.videos ?.firstOrNull { video -> video.season == launch.seasonNumber && - video.episode == launch.episodeNumber + video.episode == launch.episodeNumber } ?.id ?.takeIf { it.isNotBlank() } @@ -1818,8 +1788,8 @@ private fun MainAppContent( }, ) } + } } - } NuvioPosterActionSheet( item = selectedPosterForActions, @@ -1857,94 +1827,73 @@ private fun MainAppContent( }.onFailure { error -> pickerError = error.message ?: getString(Res.string.trakt_lists_load_failed) } - }.onFailure { error -> - pickerError = error.message ?: getString(Res.string.trakt_lists_load_failed) + pickerPending = false } - pickerPending = false } } - } - }, - onToggleWatched = { - selectedPosterForActions?.let { preview -> - coroutineScope.launch { - WatchingActions.togglePosterWatched(preview) + }, + onToggleWatched = { + selectedPosterForActions?.let { preview -> + coroutineScope.launch { + WatchingActions.togglePosterWatched(preview) + } } - } - }, - ) + }, + ) - NuvioContinueWatchingActionSheet( - item = selectedContinueWatchingForActions, - showManualPlayOption = StreamAutoPlayPolicy.isEffectivelyEnabled(playerSettingsUiState), - onDismiss = { selectedContinueWatchingForActions = null }, - onOpenDetails = { - selectedContinueWatchingForActions?.let { item -> - navController.navigate( - DetailRoute( - type = item.parentMetaType, - id = item.parentMetaId, - ), - ) - } - }, - onStartFromBeginning = selectedContinueWatchingForActions - ?.takeIf { !it.isNextUp } - ?.let { item -> { onContinueWatchingStartFromBeginning(item) } }, - onPlayManually = selectedContinueWatchingForActions - ?.let { item -> { onContinueWatchingPlayManually(item) } }, - onRemove = { - selectedContinueWatchingForActions?.let { item -> - if (item.isNextUp) { - ContinueWatchingPreferencesRepository.addDismissedNextUpKey( - nextUpDismissKey( - item.parentMetaId, - item.nextUpSeedSeasonNumber, - item.nextUpSeedEpisodeNumber, + NuvioContinueWatchingActionSheet( + item = selectedContinueWatchingForActions, + showManualPlayOption = StreamAutoPlayPolicy.isEffectivelyEnabled(playerSettingsUiState), + onDismiss = { selectedContinueWatchingForActions = null }, + onOpenDetails = { + selectedContinueWatchingForActions?.let { item -> + navController.navigate( + DetailRoute( + type = item.parentMetaType, + id = item.parentMetaId, ), ) - } else { - WatchProgressRepository.removeProgress(contentId = item.parentMetaId) } - } - }, - ) + }, + onStartFromBeginning = selectedContinueWatchingForActions + ?.takeIf { !it.isNextUp } + ?.let { item -> { onContinueWatchingStartFromBeginning(item) } }, + onPlayManually = selectedContinueWatchingForActions + ?.let { item -> { onContinueWatchingPlayManually(item) } }, + onRemove = { + selectedContinueWatchingForActions?.let { item -> + if (item.isNextUp) { + ContinueWatchingPreferencesRepository.addDismissedNextUpKey( + nextUpDismissKey( + item.parentMetaId, + item.nextUpSeedSeasonNumber, + item.nextUpSeedEpisodeNumber, + ), + ) + } else { + WatchProgressRepository.removeProgress(contentId = item.parentMetaId) + } + } + }, + ) - TraktListPickerDialog( - visible = showLibraryListPicker, - title = pickerTitle, - tabs = pickerTabs, - membership = pickerMembership, - isPending = pickerPending, - errorMessage = pickerError, - onToggle = { listKey -> - pickerMembership = pickerMembership.toMutableMap().apply { - this[listKey] = !(this[listKey] == true) - } - }, - onDismiss = { - if (!pickerPending) { - showLibraryListPicker = false - pickerItem = null - pickerError = null - } - }, - onSave = { - val item = pickerItem ?: return@TraktListPickerDialog - coroutineScope.launch { - pickerPending = true - pickerError = null - runCatching { - LibraryRepository.applyMembershipChanges( - item = item, - desiredMembership = pickerMembership, - ) - }.onSuccess { + TraktListPickerDialog( + visible = showLibraryListPicker, + title = pickerTitle, + tabs = pickerTabs, + membership = pickerMembership, + isPending = pickerPending, + errorMessage = pickerError, + onToggle = { listKey -> + pickerMembership = pickerMembership.toMutableMap().apply { + this[listKey] = !(this[listKey] == true) + } + }, + onDismiss = { + if (!pickerPending) { showLibraryListPicker = false pickerItem = null pickerError = null - }.onFailure { error -> - pickerError = error.message ?: getString(Res.string.trakt_lists_update_failed) } }, onSave = { @@ -1984,22 +1933,22 @@ private fun MainAppContent( }, ) - androidx.compose.animation.AnimatedVisibility( - visible = !initialHomeReady || profileSwitchLoading, - enter = fadeIn(), - exit = fadeOut(androidx.compose.animation.core.tween(400)), - ) { - AppLaunchOverlay(modifier = Modifier.fillMaxSize()) - } - - // Auto-dismiss profile switch overlay - if (profileSwitchLoading) { - LaunchedEffect(Unit) { - // Brief loading screen while home refreshes for the new profile - kotlinx.coroutines.delay(1200) - profileSwitchLoading = false + androidx.compose.animation.AnimatedVisibility( + visible = !initialHomeReady || profileSwitchLoading, + enter = fadeIn(), + exit = fadeOut(androidx.compose.animation.core.tween(400)), + ) { + AppLaunchOverlay(modifier = Modifier.fillMaxSize()) + } + + // Auto-dismiss profile switch overlay + if (profileSwitchLoading) { + LaunchedEffect(Unit) { + // Brief loading screen while home refreshes for the new profile + kotlinx.coroutines.delay(1200) + profileSwitchLoading = false + } } - } NuvioFloatingPrompt( visible = resumePromptItem != null, @@ -2019,19 +1968,19 @@ private fun MainAppContent( .zIndex(15f), ) - NuvioToastHost( - modifier = Modifier - .align(Alignment.TopCenter) - .zIndex(20f), - ) + NuvioToastHost( + modifier = Modifier + .align(Alignment.TopCenter) + .zIndex(20f), + ) - AppUpdaterHost( - controller = appUpdaterController, - modifier = Modifier - .align(Alignment.Center) - .zIndex(25f), - ) - } + AppUpdaterHost( + controller = appUpdaterController, + modifier = Modifier + .align(Alignment.Center) + .zIndex(25f), + ) + } } @Composable @@ -2063,7 +2012,6 @@ private fun AppTabHost( onPosterClick: ((MetaPreview) -> Unit)? = null, onPosterLongClick: ((MetaPreview) -> Unit)? = null, onLibraryPosterClick: ((LibraryItem) -> Unit)? = null, - onLibraryPosterLongClick: ((LibraryItem) -> Unit)? = null, onLibrarySectionViewAllClick: ((LibrarySection) -> Unit)? = null, onContinueWatchingClick: ((ContinueWatchingItem) -> Unit)? = null, onContinueWatchingLongPress: ((ContinueWatchingItem) -> Unit)? = null, @@ -2113,7 +2061,6 @@ private fun AppTabHost( LibraryScreen( modifier = Modifier.fillMaxSize(), onPosterClick = onLibraryPosterClick, - onPosterLongClick = onLibraryPosterLongClick, onSectionViewAllClick = onLibrarySectionViewAllClick, ) } From 31364ce401e4480d5bd4e6f33a39d7254e89e899 Mon Sep 17 00:00:00 2001 From: AdityasahuX07 Date: Tue, 12 May 2026 23:26:26 +0530 Subject: [PATCH 6/6] Update LibraryScreen.kt --- .../nuvio/app/features/library/LibraryScreen.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 85623512..11989a4c 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 @@ -53,7 +53,10 @@ fun LibraryScreen( LibraryRepository.uiState }.collectAsStateWithLifecycle() val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle() - val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle() + val watchedUiState by remember { + WatchedRepository.ensureLoaded() + WatchedRepository.uiState + }.collectAsStateWithLifecycle() var actionTarget by remember { mutableStateOf(null) } var observedOfflineState by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() @@ -159,6 +162,7 @@ fun LibraryScreen( else -> { librarySections( sections = uiState.sections, + watchedKeys = watchedUiState.watchedKeys, onPosterClick = onPosterClick, onSectionViewAllClick = onSectionViewAllClick, onPosterLongClick = { item, section -> @@ -213,6 +217,7 @@ fun LibraryScreen( private fun LazyListScope.librarySections( sections: List, + watchedKeys: Set, onPosterClick: ((LibraryItem) -> Unit)?, onSectionViewAllClick: ((LibrarySection) -> Unit)?, onPosterLongClick: (LibraryItem, LibrarySection) -> Unit, @@ -235,8 +240,13 @@ private fun LazyListScope.librarySections( viewAllPillSize = NuvioViewAllPillSize.Compact, key = { item -> "${item.type}:${item.id}" }, ) { item -> + val preview = item.toMetaPreview() HomePosterCard( - item = item.toMetaPreview(), + item = preview, + isWatched = WatchingState.isPosterWatched( + watchedKeys = watchedKeys, + item = preview, + ), onClick = onPosterClick?.let { { it(item) } }, onLongClick = { onPosterLongClick(item, section) }, )