mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat: init continue where you left off popup after exiting directly the app from player
This commit is contained in:
parent
2844f74503
commit
e54f90ee63
13 changed files with 442 additions and 3 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ContinueWatchingItem?>(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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ fun ContinueWatchingSettingsScreen(
|
|||
isVisible = continueWatchingPreferencesUiState.isVisible,
|
||||
style = continueWatchingPreferencesUiState.style,
|
||||
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
||||
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ private data class StoredContinueWatchingPreferences(
|
|||
val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Wide,
|
||||
val upNextFromFurthestEpisode: Boolean = true,
|
||||
val dismissedNextUpKeys: Set<String> = emptySet(),
|
||||
val showResumePromptOnLaunch: Boolean = true,
|
||||
)
|
||||
|
||||
object ContinueWatchingPreferencesRepository {
|
||||
|
|
@ -48,7 +49,7 @@ object ContinueWatchingPreferencesRepository {
|
|||
dismissedNextUpKeys: Set<String>,
|
||||
) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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?)
|
||||
}
|
||||
|
|
@ -164,6 +164,7 @@ data class ContinueWatchingPreferencesUiState(
|
|||
val style: ContinueWatchingSectionStyle = ContinueWatchingSectionStyle.Wide,
|
||||
val upNextFromFurthestEpisode: Boolean = true,
|
||||
val dismissedNextUpKeys: Set<String> = emptySet(),
|
||||
val showResumePromptOnLaunch: Boolean = true,
|
||||
)
|
||||
|
||||
internal fun nextUpDismissKey(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
CURRENT_PROJECT_VERSION=15
|
||||
MARKETING_VERSION=0.1.0-alpha15
|
||||
CURRENT_PROJECT_VERSION=16
|
||||
MARKETING_VERSION=0.1.0-alpha16
|
||||
Loading…
Reference in a new issue