diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt index abee26728..d2ed9f46d 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt @@ -32,6 +32,7 @@ import com.nuvio.app.features.watched.WatchedStorage import com.nuvio.app.features.streams.StreamLinkCacheStorage import com.nuvio.app.features.watchprogress.ContinueWatchingEnrichmentStorage import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage +import com.nuvio.app.features.watchprogress.ResumePromptStorage import com.nuvio.app.features.watchprogress.WatchProgressStorage class MainActivity : ComponentActivity() { @@ -60,6 +61,7 @@ class MainActivity : ComponentActivity() { TraktAuthStorage.initialize(applicationContext) TraktCommentsStorage.initialize(applicationContext) ContinueWatchingPreferencesStorage.initialize(applicationContext) + ResumePromptStorage.initialize(applicationContext) ContinueWatchingEnrichmentStorage.initialize(applicationContext) EpisodeReleaseNotificationsStorage.initialize(applicationContext) WatchProgressStorage.initialize(applicationContext) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptStorage.android.kt new file mode 100644 index 000000000..31c1196d3 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptStorage.android.kt @@ -0,0 +1,38 @@ +package com.nuvio.app.features.watchprogress + +import android.content.Context +import android.content.SharedPreferences +import com.nuvio.app.core.storage.ProfileScopedKey + +actual object ResumePromptStorage { + private const val preferencesName = "nuvio_resume_prompt" + private const val wasInPlayerKey = "was_in_player" + private const val lastPlayerVideoIdKey = "last_player_video_id" + + private var preferences: SharedPreferences? = null + + fun initialize(context: Context) { + preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE) + } + + actual fun loadWasInPlayer(): Boolean = + preferences?.getBoolean(ProfileScopedKey.of(wasInPlayerKey), false) ?: false + + actual fun saveWasInPlayer(value: Boolean) { + preferences?.edit()?.putBoolean(ProfileScopedKey.of(wasInPlayerKey), value)?.apply() + } + + actual fun loadLastPlayerVideoId(): String? = + preferences?.getString(ProfileScopedKey.of(lastPlayerVideoIdKey), null) + + actual fun saveLastPlayerVideoId(videoId: String?) { + preferences?.edit()?.apply { + if (videoId != null) { + putString(ProfileScopedKey.of(lastPlayerVideoIdKey), videoId) + } else { + remove(ProfileScopedKey.of(lastPlayerVideoIdKey)) + } + apply() + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 111c06d7f..7bab3185a 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -79,6 +79,7 @@ import com.nuvio.app.core.ui.NuvioPosterActionSheet import com.nuvio.app.core.ui.PlatformBackHandler import com.nuvio.app.core.ui.configurePlatformImageLoader import com.nuvio.app.core.ui.NuvioToastHost +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.features.auth.AuthScreen @@ -142,6 +143,7 @@ import com.nuvio.app.features.trakt.TraktListTab import com.nuvio.app.features.watched.WatchedRepository import com.nuvio.app.features.watchprogress.ContinueWatchingItem import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository +import com.nuvio.app.features.watchprogress.ResumePromptRepository import com.nuvio.app.features.watchprogress.WatchProgressRepository import com.nuvio.app.features.watchprogress.nextUpDismissKey import com.nuvio.app.features.watching.application.WatchingActions @@ -450,6 +452,24 @@ private fun MainAppContent( } } var profileSwitchLoading by remember { mutableStateOf(false) } + var resumePromptItem by remember { mutableStateOf(null) } + val continueWatchingPreferencesUiState by remember { + ContinueWatchingPreferencesRepository.ensureLoaded() + ContinueWatchingPreferencesRepository.uiState + }.collectAsStateWithLifecycle() + + LaunchedEffect( + initialHomeReady, + profileSwitchLoading, + profileState.activeProfile?.profileIndex, + continueWatchingPreferencesUiState.showResumePromptOnLaunch, + ) { + if (!initialHomeReady || profileSwitchLoading) return@LaunchedEffect + if (resumePromptItem != null) return@LaunchedEffect + if (continueWatchingPreferencesUiState.showResumePromptOnLaunch) { + resumePromptItem = ResumePromptRepository.consumeResumePrompt() + } + } LaunchedEffect(navController) { AppDeepLinkRepository.pendingDeepLink.collectLatest { deepLink -> @@ -1120,6 +1140,9 @@ private fun MainAppContent( Box(modifier = Modifier.fillMaxSize()) return@composable } + LaunchedEffect(launch.videoId) { + launch.videoId?.let { ResumePromptRepository.markPlayerEntered(it) } + } PlayerScreen( title = launch.title, sourceUrl = launch.sourceUrl, @@ -1146,6 +1169,7 @@ private fun MainAppContent( initialPositionMs = launch.initialPositionMs, initialProgressFraction = launch.initialProgressFraction, onBack = { + ResumePromptRepository.markPlayerExitedNormally() PlayerLaunchStore.remove(route.launchId) navController.popBackStack() }, @@ -1412,6 +1436,39 @@ private fun MainAppContent( } } + 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 + onPlay( + item.parentMetaType, + item.videoId, + item.parentMetaId, + item.parentMetaType, + item.title, + item.logo, + item.poster, + item.background, + item.seasonNumber, + item.episodeNumber, + item.episodeTitle, + item.episodeThumbnail, + item.pauseDescription, + item.resumePositionMs, + ) + }, + onDismiss = { resumePromptItem = null }, + modifier = Modifier + .align(Alignment.BottomCenter) + .zIndex(15f), + ) + NuvioToastHost( modifier = Modifier .align(Alignment.TopCenter) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioFloatingPrompt.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioFloatingPrompt.kt new file mode 100644 index 000000000..fee262ae7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioFloatingPrompt.kt @@ -0,0 +1,246 @@ +package com.nuvio.app.core.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectVerticalDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import kotlinx.coroutines.delay +import kotlin.math.roundToInt + +private const val AutoDismissDelayMs = 15_000L +private const val SwipeDismissThreshold = 80f + +@Composable +fun NuvioFloatingPrompt( + visible: Boolean, + imageUrl: String?, + title: String, + subtitle: String, + progressFraction: Float, + actionLabel: String, + onAction: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + autoDismissMs: Long = AutoDismissDelayMs, +) { + val visibilityState = remember { MutableTransitionState(false) } + val hapticFeedback = LocalHapticFeedback.current + val dismissWithHaptic = remember(hapticFeedback, onDismiss) { + { + hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove) + onDismiss() + } + } + val actionWithHaptic = remember(hapticFeedback, onAction) { + { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onAction() + } + } + + LaunchedEffect(visible) { + visibilityState.targetState = visible + if (visible) { + hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove) + } + } + + if (visible) { + LaunchedEffect(Unit) { + delay(autoDismissMs) + onDismiss() + } + } + + val navBarBottom = nuvioBottomNavigationBarInsets() + .asPaddingValues() + .calculateBottomPadding() + + AnimatedVisibility( + visibleState = visibilityState, + modifier = modifier, + enter = fadeIn(tween(400)) + slideInVertically(tween(400)) { it }, + exit = fadeOut(tween(300)) + slideOutVertically(tween(300)) { it }, + ) { + var dragOffsetY by remember { mutableFloatStateOf(0f) } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = navBarBottom + 72.dp) + .padding(horizontal = 16.dp) + .offset { IntOffset(0, dragOffsetY.roundToInt().coerceAtLeast(0)) } + .pointerInput(Unit) { + detectVerticalDragGestures( + onDragEnd = { + if (dragOffsetY > SwipeDismissThreshold) { + dismissWithHaptic() + } + dragOffsetY = 0f + }, + onDragCancel = { dragOffsetY = 0f }, + ) { _, dragAmount -> + dragOffsetY = (dragOffsetY + dragAmount).coerceAtLeast(0f) + } + }, + contentAlignment = Alignment.BottomCenter, + ) { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 4.dp, + shadowElevation = 8.dp, + ) { + Column( + modifier = Modifier.padding(horizontal = 14.dp, vertical = 14.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + modifier = Modifier + .size(width = 54.dp, height = 78.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center, + ) { + if (imageUrl != null) { + AsyncImage( + model = imageUrl, + contentDescription = null, + modifier = Modifier.matchParentSize(), + contentScale = ContentScale.Crop, + ) + } + } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "Continue where you left off", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = title, + style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + IconButton( + onClick = dismissWithHaptic, + modifier = Modifier.size(28.dp), + colors = IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.68f), + ), + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = "Dismiss", + modifier = Modifier.size(14.dp), + ) + } + + FilledIconButton( + onClick = actionWithHaptic, + modifier = Modifier.size(42.dp), + shape = CircleShape, + ) { + Icon( + imageVector = Icons.Rounded.PlayArrow, + contentDescription = actionLabel, + modifier = Modifier.size(22.dp), + ) + } + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(999.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f)) + .padding(1.dp), + ) { + LinearProgressIndicator( + progress = { progressFraction.coerceIn(0f, 1f) }, + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .clip(RoundedCornerShape(999.dp)), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f), + ) + } + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt index a38a07095..8668f0e8d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContinueWatchingSettingsPage.kt @@ -31,6 +31,7 @@ internal fun LazyListScope.continueWatchingSettingsContent( isVisible: Boolean, style: ContinueWatchingSectionStyle, upNextFromFurthestEpisode: Boolean, + showResumePromptOnLaunch: Boolean, ) { item { SettingsSection( @@ -76,6 +77,22 @@ internal fun LazyListScope.continueWatchingSettingsContent( } } } + item { + SettingsSection( + title = "ON LAUNCH", + isTablet = isTablet, + ) { + SettingsGroup(isTablet = isTablet) { + SettingsSwitchRow( + title = "Resume prompt on launch", + description = "Show a popup to continue where you left off when opening the app after leaving from the player.", + checked = showResumePromptOnLaunch, + isTablet = isTablet, + onCheckedChange = ContinueWatchingPreferencesRepository::setShowResumePromptOnLaunch, + ) + } + } + } } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsFullScreenPages.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsFullScreenPages.kt index aa80f8af6..f23a4fc08 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsFullScreenPages.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsFullScreenPages.kt @@ -119,6 +119,7 @@ fun ContinueWatchingSettingsScreen( isVisible = continueWatchingPreferencesUiState.isVisible, style = continueWatchingPreferencesUiState.style, upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode, + showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch, ) } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt index 2408f7d32..be3c734c4 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt @@ -315,6 +315,7 @@ private fun MobileSettingsScreen( isVisible = continueWatchingPreferencesUiState.isVisible, style = continueWatchingPreferencesUiState.style, upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode, + showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch, ) SettingsPage.ContentDiscovery -> contentDiscoveryContent( isTablet = false, @@ -516,6 +517,7 @@ private fun TabletSettingsScreen( isVisible = continueWatchingPreferencesUiState.isVisible, style = continueWatchingPreferencesUiState.style, upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode, + showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch, ) SettingsPage.ContentDiscovery -> contentDiscoveryContent( isTablet = true, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt index f43d5640e..5e0eb093b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ContinueWatchingPreferencesRepository.kt @@ -14,6 +14,7 @@ private data class StoredContinueWatchingPreferences( val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Wide, val upNextFromFurthestEpisode: Boolean = true, val dismissedNextUpKeys: Set = emptySet(), + val showResumePromptOnLaunch: Boolean = true, ) object ContinueWatchingPreferencesRepository { @@ -48,7 +49,7 @@ object ContinueWatchingPreferencesRepository { dismissedNextUpKeys: Set, ) { ensureLoaded() - _uiState.value = ContinueWatchingPreferencesUiState( + _uiState.value = _uiState.value.copy( isVisible = isVisible, style = style, upNextFromFurthestEpisode = upNextFromFurthestEpisode, @@ -79,6 +80,7 @@ object ContinueWatchingPreferencesRepository { style = stored.style, upNextFromFurthestEpisode = stored.upNextFromFurthestEpisode, dismissedNextUpKeys = stored.dismissedNextUpKeys, + showResumePromptOnLaunch = stored.showResumePromptOnLaunch, ) } else { ContinueWatchingPreferencesUiState() @@ -113,6 +115,12 @@ object ContinueWatchingPreferencesRepository { persist() } + fun setShowResumePromptOnLaunch(enabled: Boolean) { + ensureLoaded() + _uiState.value = _uiState.value.copy(showResumePromptOnLaunch = enabled) + persist() + } + fun removeDismissedNextUpKeysForContent(contentId: String) { ensureLoaded() val normalizedContentId = contentId.trim() @@ -132,6 +140,7 @@ object ContinueWatchingPreferencesRepository { style = _uiState.value.style, upNextFromFurthestEpisode = _uiState.value.upNextFromFurthestEpisode, dismissedNextUpKeys = _uiState.value.dismissedNextUpKeys, + showResumePromptOnLaunch = _uiState.value.showResumePromptOnLaunch, ), ), ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptRepository.kt new file mode 100644 index 000000000..82163b5ba --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptRepository.kt @@ -0,0 +1,31 @@ +package com.nuvio.app.features.watchprogress + +object ResumePromptRepository { + + fun markPlayerEntered(videoId: String) { + ResumePromptStorage.saveWasInPlayer(true) + ResumePromptStorage.saveLastPlayerVideoId(videoId) + } + + fun markPlayerExitedNormally() { + ResumePromptStorage.saveWasInPlayer(false) + ResumePromptStorage.saveLastPlayerVideoId(null) + } + + fun consumeResumePrompt(): ContinueWatchingItem? { + val wasInPlayer = ResumePromptStorage.loadWasInPlayer() + if (!wasInPlayer) return null + + val videoId = ResumePromptStorage.loadLastPlayerVideoId() + ResumePromptStorage.saveWasInPlayer(false) + ResumePromptStorage.saveLastPlayerVideoId(null) + + if (videoId.isNullOrBlank()) return null + + WatchProgressRepository.ensureLoaded() + val entry = WatchProgressRepository.progressForVideo(videoId) ?: return null + if (!entry.isResumable) return null + + return entry.toContinueWatchingItem() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptStorage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptStorage.kt new file mode 100644 index 000000000..793a19d02 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptStorage.kt @@ -0,0 +1,8 @@ +package com.nuvio.app.features.watchprogress + +internal expect object ResumePromptStorage { + fun loadWasInPlayer(): Boolean + fun saveWasInPlayer(value: Boolean) + fun loadLastPlayerVideoId(): String? + fun saveLastPlayerVideoId(videoId: String?) +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt index a25510e0d..855596c5b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/watchprogress/WatchProgressModels.kt @@ -164,6 +164,7 @@ data class ContinueWatchingPreferencesUiState( val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Wide, val upNextFromFurthestEpisode: Boolean = true, val dismissedNextUpKeys: Set = emptySet(), + val showResumePromptOnLaunch: Boolean = true, ) internal fun nextUpDismissKey( diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptStorage.ios.kt new file mode 100644 index 000000000..8a17058e3 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/watchprogress/ResumePromptStorage.ios.kt @@ -0,0 +1,27 @@ +package com.nuvio.app.features.watchprogress + +import com.nuvio.app.core.storage.ProfileScopedKey +import platform.Foundation.NSUserDefaults + +actual object ResumePromptStorage { + private const val wasInPlayerKey = "nuvio_resume_prompt_was_in_player" + private const val lastPlayerVideoIdKey = "nuvio_resume_prompt_last_player_video_id" + + actual fun loadWasInPlayer(): Boolean = + NSUserDefaults.standardUserDefaults.boolForKey(ProfileScopedKey.of(wasInPlayerKey)) + + actual fun saveWasInPlayer(value: Boolean) { + NSUserDefaults.standardUserDefaults.setBool(value, forKey = ProfileScopedKey.of(wasInPlayerKey)) + } + + actual fun loadLastPlayerVideoId(): String? = + NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(lastPlayerVideoIdKey)) + + actual fun saveLastPlayerVideoId(videoId: String?) { + if (videoId != null) { + NSUserDefaults.standardUserDefaults.setObject(videoId, forKey = ProfileScopedKey.of(lastPlayerVideoIdKey)) + } else { + NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(lastPlayerVideoIdKey)) + } + } +} diff --git a/iosApp/Configuration/Version.xcconfig b/iosApp/Configuration/Version.xcconfig index e7b6ea3aa..5b71efe56 100644 --- a/iosApp/Configuration/Version.xcconfig +++ b/iosApp/Configuration/Version.xcconfig @@ -1,2 +1,2 @@ -CURRENT_PROJECT_VERSION=15 -MARKETING_VERSION=0.1.0-alpha15 \ No newline at end of file +CURRENT_PROJECT_VERSION=16 +MARKETING_VERSION=0.1.0-alpha16 \ No newline at end of file