From 8187f404c4cfff15a22a97ab30bb0b13b1ff246a Mon Sep 17 00:00:00 2001 From: AdityasahuX07 Date: Sat, 25 Apr 2026 13:10:39 +0530 Subject: [PATCH] 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),