diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 30e9ef75..4058c118 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -126,6 +126,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 @@ -276,6 +277,12 @@ data class CatalogRoute( val genre: String? = null, ) +private data class PosterActionTarget( + val preview: MetaPreview, + val libraryItem: LibraryItem? = null, + val libraryListKey: String? = null, +) + enum class AppScreenTab { Home, Search, @@ -556,7 +563,7 @@ private fun MainAppContent( }.collectAsStateWithLifecycle() val liquidGlassNativeTabBarSupported = remember { isLiquidGlassNativeTabBarSupported() } var showExitConfirmation by rememberSaveable { mutableStateOf(false) } - var selectedPosterForActions by remember { mutableStateOf(null) } + var selectedPosterActionTarget by remember { mutableStateOf(null) } var selectedContinueWatchingForActions by remember { mutableStateOf(null) } var showLibraryListPicker by remember { mutableStateOf(false) } var pickerItem by remember { mutableStateOf(null) } @@ -1136,11 +1143,19 @@ private fun MainAppContent( }, onPosterLongClick = { meta -> hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - selectedPosterForActions = meta + selectedPosterActionTarget = PosterActionTarget(preview = meta) }, onLibraryPosterClick = { item -> navController.navigate(DetailRoute(type = item.type, id = item.id)) }, + onLibraryPosterLongClick = { item, section -> + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + selectedPosterActionTarget = PosterActionTarget( + preview = item.toMetaPreview(), + libraryItem = item, + libraryListKey = section.type, + ) + }, onLibrarySectionViewAllClick = onLibrarySectionViewAllClick, onContinueWatchingClick = onContinueWatchingClick, onContinueWatchingLongPress = onContinueWatchingLongPress, @@ -1832,6 +1847,18 @@ private fun MainAppContent( onPosterClick = { meta -> navController.navigate(DetailRoute(type = meta.type, id = meta.id)) }, + onPosterLongClick = { meta -> + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + selectedPosterActionTarget = if (route.manifestUrl == INTERNAL_LIBRARY_MANIFEST_URL) { + PosterActionTarget( + preview = meta, + libraryItem = meta.toLibraryItem(savedAtEpochMs = 0L), + libraryListKey = route.catalogId, + ) + } else { + PosterActionTarget(preview = meta) + } + }, modifier = Modifier.fillMaxSize(), ) } @@ -1997,48 +2024,74 @@ private fun MainAppContent( } NuvioPosterActionSheet( - item = selectedPosterForActions, - isSaved = selectedPosterForActions?.let { preview -> + item = selectedPosterActionTarget?.preview, + isSaved = selectedPosterActionTarget?.preview?.let { preview -> LibraryRepository.isSaved(preview.id, preview.type) } == true, - isWatched = selectedPosterForActions?.let { preview -> + isWatched = selectedPosterActionTarget?.preview?.let { preview -> WatchingState.isPosterWatched( watchedKeys = watchedUiState.watchedKeys, item = preview, ) } == true, - onDismiss = { selectedPosterForActions = null }, + onDismiss = { selectedPosterActionTarget = null }, onToggleLibrary = { - selectedPosterForActions?.let { preview -> - val libraryItem = preview.toLibraryItem(savedAtEpochMs = 0L) - if (!isTraktLibrarySource) { - LibraryRepository.toggleSaved(libraryItem) - } else { - pickerItem = libraryItem - pickerTitle = preview.name - pickerTabs = LibraryRepository.libraryListTabs() - pickerMembership = pickerTabs.associate { it.key to false } - pickerPending = true - pickerError = null - showLibraryListPicker = true - coroutineScope.launch { - runCatching { - val snapshot = LibraryRepository.getMembershipSnapshot(libraryItem) - val tabs = LibraryRepository.libraryListTabs() - pickerTabs = tabs - pickerMembership = tabs.associate { tab -> - tab.key to (snapshot[tab.key] == true) + selectedPosterActionTarget?.let { target -> + val preview = target.preview + val libraryItem = target.libraryItem ?: preview.toLibraryItem(savedAtEpochMs = 0L) + if (target.libraryItem != null) { + if (isTraktLibrarySource) { + coroutineScope.launch { + runCatching { + val listKey = target.libraryListKey + if (listKey.isNullOrBlank()) { + val currentMembership = LibraryRepository.getMembershipSnapshot(libraryItem) + LibraryRepository.applyMembershipChanges( + item = libraryItem, + desiredMembership = currentMembership.mapValues { false }, + ) + } else { + LibraryRepository.removeFromList(libraryItem, listKey) + } + }.onFailure { error -> + NuvioToastController.show( + error.message ?: getString(Res.string.trakt_lists_update_failed), + ) } - }.onFailure { error -> - pickerError = error.message ?: getString(Res.string.trakt_lists_load_failed) } - pickerPending = false + } else { + LibraryRepository.remove(libraryItem.id) + } + } else { + if (!isTraktLibrarySource) { + LibraryRepository.toggleSaved(libraryItem) + } else { + pickerItem = libraryItem + pickerTitle = preview.name + pickerTabs = LibraryRepository.libraryListTabs() + pickerMembership = pickerTabs.associate { it.key to false } + pickerPending = true + pickerError = null + showLibraryListPicker = true + coroutineScope.launch { + runCatching { + val snapshot = LibraryRepository.getMembershipSnapshot(libraryItem) + val tabs = LibraryRepository.libraryListTabs() + 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 -> + selectedPosterActionTarget?.preview?.let { preview -> coroutineScope.launch { WatchingActions.togglePosterWatched(preview) } @@ -2223,6 +2276,7 @@ private fun AppTabHost( onPosterClick: ((MetaPreview) -> Unit)? = null, onPosterLongClick: ((MetaPreview) -> Unit)? = null, onLibraryPosterClick: ((LibraryItem) -> Unit)? = null, + onLibraryPosterLongClick: ((LibraryItem, LibrarySection) -> Unit)? = null, onLibrarySectionViewAllClick: ((LibrarySection) -> Unit)? = null, onContinueWatchingClick: ((ContinueWatchingItem) -> Unit)? = null, onContinueWatchingLongPress: ((ContinueWatchingItem) -> Unit)? = null, @@ -2276,6 +2330,7 @@ private fun AppTabHost( modifier = Modifier.fillMaxSize(), scrollToTopRequests = libraryScrollToTopRequests, onPosterClick = onLibraryPosterClick, + onPosterLongClick = onLibraryPosterLongClick, onSectionViewAllClick = onLibrarySectionViewAllClick, ) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt index f58cd2df..c611b161 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt @@ -72,6 +72,7 @@ fun CatalogScreen( genre: String? = null, onBack: () -> Unit, onPosterClick: ((MetaPreview) -> Unit)? = null, + onPosterLongClick: ((MetaPreview) -> Unit)? = null, modifier: Modifier = Modifier, ) { val uiState by CatalogRepository.uiState.collectAsStateWithLifecycle() @@ -187,6 +188,7 @@ fun CatalogScreen( cornerRadiusDp = posterCardStyle.cornerRadiusDp, hideLabels = posterCardStyle.hideLabelsEnabled, onClick = onPosterClick?.let { { it(item) } }, + onLongClick = onPosterLongClick?.let { { it(item) } }, ) } if (uiState.isLoading) { @@ -257,6 +259,7 @@ private fun CatalogPosterTile( cornerRadiusDp: Int, hideLabels: Boolean, onClick: (() -> Unit)? = null, + onLongClick: (() -> Unit)? = null, ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), @@ -267,7 +270,7 @@ private fun CatalogPosterTile( .aspectRatio(item.posterShape.catalogAspectRatio()) .clip(RoundedCornerShape(cornerRadiusDp.dp)) .background(MaterialTheme.colorScheme.surface) - .posterCardClickable(onClick = onClick, onLongClick = null), + .posterCardClickable(onClick = onClick, onLongClick = onLongClick), ) { if (item.poster != null) { AsyncImage( 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 7febd8ea..dc75e101 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 @@ -25,8 +25,6 @@ import com.nuvio.app.core.network.NetworkStatusRepository 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 import com.nuvio.app.core.ui.nuvioBlockPointerPassthrough @@ -38,20 +36,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow 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( - val item: LibraryItem, - val listKey: String? = null, - val listTitle: String? = null, -) - @Composable fun LibraryScreen( modifier: Modifier = Modifier, scrollToTopRequests: Flow = emptyFlow(), onPosterClick: ((LibraryItem) -> Unit)? = null, + onPosterLongClick: ((LibraryItem, LibrarySection) -> Unit)? = null, onSectionViewAllClick: ((LibrarySection) -> Unit)? = null, ) { val uiState by remember { @@ -59,7 +51,6 @@ fun LibraryScreen( LibraryRepository.uiState }.collectAsStateWithLifecycle() val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle() - var pendingRemovalTarget by remember { mutableStateOf(null) } var observedOfflineState by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() val listState = rememberLazyListState() @@ -187,64 +178,18 @@ fun LibraryScreen( sections = uiState.sections, onPosterClick = onPosterClick, onSectionViewAllClick = onSectionViewAllClick, - onPosterLongClick = { item, section -> - pendingRemovalTarget = if (isTraktSource) { - LibraryRemovalTarget( - item = item, - listKey = section.type, - listTitle = section.displayTitle, - ) - } else { - LibraryRemovalTarget(item = item) - } - }, + onPosterLongClick = onPosterLongClick, ) } } } - - 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 { - coroutineScope.launch { - runCatching { - LibraryRepository.removeFromList(target.item, listKey) - }.onFailure { error -> - NuvioToastController.show( - error.message ?: getString(Res.string.trakt_lists_update_failed), - ) - } - } - } - } - }, - onDismiss = { pendingRemovalTarget = null }, - ) } private fun LazyListScope.librarySections( sections: List, onPosterClick: ((LibraryItem) -> Unit)?, onSectionViewAllClick: ((LibrarySection) -> Unit)?, - onPosterLongClick: (LibraryItem, LibrarySection) -> Unit, + onPosterLongClick: ((LibraryItem, LibrarySection) -> Unit)?, ) { items( items = sections, @@ -267,7 +212,7 @@ private fun LazyListScope.librarySections( HomePosterCard( item = item.toMetaPreview(), onClick = onPosterClick?.let { { it(item) } }, - onLongClick = { onPosterLongClick(item, section) }, + onLongClick = onPosterLongClick?.let { { it(item, section) } }, ) } }