mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat: Enhance profile management with profile switching functionality
- Updated MainAppContent to support profile switching. - Added onSwitchProfile callback to SettingsScreen and related components. - Introduced a new "Switch Profile" option in the settings menu for easier profile management. - Improved watch progress synchronization by resolving remote metadata for entries.
This commit is contained in:
parent
b8f4afce5d
commit
d9fd0e49d5
4 changed files with 89 additions and 5 deletions
|
|
@ -204,7 +204,11 @@ fun App() {
|
|||
)
|
||||
}
|
||||
AppGateScreen.Main.name -> {
|
||||
MainAppContent()
|
||||
MainAppContent(
|
||||
onSwitchProfile = {
|
||||
gateScreen = AppGateScreen.ProfileSelection.name
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -213,7 +217,9 @@ fun App() {
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun MainAppContent() {
|
||||
private fun MainAppContent(
|
||||
onSwitchProfile: () -> Unit = {},
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) }
|
||||
|
|
@ -360,6 +366,7 @@ private fun MainAppContent() {
|
|||
},
|
||||
onContinueWatchingClick = onContinueWatchingClick,
|
||||
onContinueWatchingLongPress = onContinueWatchingLongPress,
|
||||
onSwitchProfile = onSwitchProfile,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -514,6 +521,7 @@ private fun AppTabHost(
|
|||
onLibraryPosterClick: ((LibraryItem) -> Unit)? = null,
|
||||
onContinueWatchingClick: ((ContinueWatchingItem) -> Unit)? = null,
|
||||
onContinueWatchingLongPress: ((ContinueWatchingItem) -> Unit)? = null,
|
||||
onSwitchProfile: (() -> Unit)? = null,
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
keepAliveTab(
|
||||
|
|
@ -550,6 +558,7 @@ private fun AppTabHost(
|
|||
) {
|
||||
SettingsScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onSwitchProfile = onSwitchProfile,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.rounded.AccountCircle
|
||||
import androidx.compose.material.icons.rounded.Extension
|
||||
import androidx.compose.material.icons.rounded.Palette
|
||||
import androidx.compose.material.icons.rounded.People
|
||||
import androidx.compose.material.icons.rounded.PlayArrow
|
||||
import androidx.compose.material.icons.rounded.Sync
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ internal fun LazyListScope.settingsRootContent(
|
|||
onContentDiscoveryClick: () -> Unit,
|
||||
onAccountClick: () -> Unit,
|
||||
onSyncOverviewClick: () -> Unit,
|
||||
onSwitchProfileClick: (() -> Unit)? = null,
|
||||
) {
|
||||
item {
|
||||
SettingsSection(
|
||||
|
|
@ -49,6 +51,15 @@ internal fun LazyListScope.settingsRootContent(
|
|||
title = "ACCOUNT & SYNC",
|
||||
isTablet = isTablet,
|
||||
) {
|
||||
if (onSwitchProfileClick != null) {
|
||||
SettingsNavigationRow(
|
||||
title = "Switch Profile",
|
||||
description = "Change to a different profile.",
|
||||
icon = Icons.Rounded.People,
|
||||
isTablet = isTablet,
|
||||
onClick = onSwitchProfileClick,
|
||||
)
|
||||
}
|
||||
SettingsNavigationRow(
|
||||
title = "Account",
|
||||
description = "Manage your account, sign out, or delete.",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
|
|||
@Composable
|
||||
fun SettingsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onSwitchProfile: (() -> Unit)? = null,
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
|
|
@ -89,6 +90,7 @@ fun SettingsScreen(
|
|||
showLoadingOverlay = playerSettingsUiState.showLoadingOverlay,
|
||||
continueWatchingVisible = continueWatchingPreferencesUiState.isVisible,
|
||||
continueWatchingStyle = continueWatchingPreferencesUiState.style,
|
||||
onSwitchProfile = onSwitchProfile,
|
||||
)
|
||||
} else {
|
||||
MobileSettingsScreen(
|
||||
|
|
@ -99,6 +101,7 @@ fun SettingsScreen(
|
|||
showLoadingOverlay = playerSettingsUiState.showLoadingOverlay,
|
||||
continueWatchingVisible = continueWatchingPreferencesUiState.isVisible,
|
||||
continueWatchingStyle = continueWatchingPreferencesUiState.style,
|
||||
onSwitchProfile = onSwitchProfile,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -113,6 +116,7 @@ private fun MobileSettingsScreen(
|
|||
showLoadingOverlay: Boolean,
|
||||
continueWatchingVisible: Boolean,
|
||||
continueWatchingStyle: ContinueWatchingSectionStyle,
|
||||
onSwitchProfile: (() -> Unit)? = null,
|
||||
) {
|
||||
NuvioScreen {
|
||||
stickyHeader {
|
||||
|
|
@ -131,6 +135,7 @@ private fun MobileSettingsScreen(
|
|||
onContentDiscoveryClick = { onPageChange(SettingsPage.ContentDiscovery) },
|
||||
onAccountClick = { onPageChange(SettingsPage.Account) },
|
||||
onSyncOverviewClick = { onPageChange(SettingsPage.SyncOverview) },
|
||||
onSwitchProfileClick = onSwitchProfile,
|
||||
)
|
||||
SettingsPage.Playback -> playbackSettingsContent(
|
||||
isTablet = false,
|
||||
|
|
@ -175,6 +180,7 @@ private fun TabletSettingsScreen(
|
|||
showLoadingOverlay: Boolean,
|
||||
continueWatchingVisible: Boolean,
|
||||
continueWatchingStyle: ContinueWatchingSectionStyle,
|
||||
onSwitchProfile: (() -> Unit)? = null,
|
||||
) {
|
||||
var selectedCategory by rememberSaveable { mutableStateOf(SettingsCategory.General.name) }
|
||||
val activeCategory = SettingsCategory.valueOf(selectedCategory)
|
||||
|
|
@ -242,6 +248,7 @@ private fun TabletSettingsScreen(
|
|||
onContentDiscoveryClick = { onPageChange(SettingsPage.ContentDiscovery) },
|
||||
onAccountClick = { onPageChange(SettingsPage.Account) },
|
||||
onSyncOverviewClick = { onPageChange(SettingsPage.SyncOverview) },
|
||||
onSwitchProfileClick = onSwitchProfile,
|
||||
)
|
||||
SettingsPage.Playback -> playbackSettingsContent(
|
||||
isTablet = true,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.nuvio.app.features.watchprogress
|
|||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.nuvio.app.core.network.SupabaseProvider
|
||||
import com.nuvio.app.features.details.MetaDetailsRepository
|
||||
import com.nuvio.app.features.player.PlayerPlaybackSnapshot
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
import io.github.jan.supabase.postgrest.postgrest
|
||||
|
|
@ -62,30 +63,86 @@ object WatchProgressRepository {
|
|||
val params = buildJsonObject { put("p_profile_id", profileId) }
|
||||
val result = SupabaseProvider.client.postgrest.rpc("sync_pull_watch_progress", params)
|
||||
val serverEntries = result.decodeList<WatchProgressSyncEntry>()
|
||||
|
||||
val oldLocal = entriesByVideoId.toMap()
|
||||
val newMap = mutableMapOf<String, WatchProgressEntry>()
|
||||
|
||||
serverEntries.forEach { entry ->
|
||||
val videoId = entry.videoId
|
||||
entriesByVideoId[videoId] = WatchProgressEntry(
|
||||
val cached = oldLocal[videoId]
|
||||
newMap[videoId] = WatchProgressEntry(
|
||||
contentType = entry.contentType,
|
||||
parentMetaId = entry.contentId,
|
||||
parentMetaType = entry.contentType,
|
||||
parentMetaType = cached?.parentMetaType ?: entry.contentType,
|
||||
videoId = videoId,
|
||||
title = "",
|
||||
title = cached?.title?.takeIf { it.isNotBlank() } ?: entry.contentId,
|
||||
logo = cached?.logo,
|
||||
poster = cached?.poster,
|
||||
background = cached?.background,
|
||||
seasonNumber = entry.season,
|
||||
episodeNumber = entry.episode,
|
||||
episodeTitle = cached?.episodeTitle,
|
||||
episodeThumbnail = cached?.episodeThumbnail,
|
||||
lastPositionMs = entry.position,
|
||||
durationMs = entry.duration,
|
||||
lastUpdatedEpochMs = entry.lastWatched,
|
||||
providerName = cached?.providerName,
|
||||
providerAddonId = cached?.providerAddonId,
|
||||
lastStreamTitle = cached?.lastStreamTitle,
|
||||
lastStreamSubtitle = cached?.lastStreamSubtitle,
|
||||
lastSourceUrl = cached?.lastSourceUrl,
|
||||
isCompleted = entry.duration > 0 && entry.position >= entry.duration,
|
||||
)
|
||||
}
|
||||
|
||||
entriesByVideoId = newMap
|
||||
hasLoaded = true
|
||||
publish()
|
||||
persist()
|
||||
|
||||
resolveRemoteMetadata()
|
||||
}.onFailure { e ->
|
||||
log.e(e) { "Failed to pull watch progress from server" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveRemoteMetadata() {
|
||||
val needsResolution = entriesByVideoId.values
|
||||
.filter { it.poster == null && it.background == null }
|
||||
.groupBy { it.parentMetaId to it.contentType }
|
||||
|
||||
if (needsResolution.isEmpty()) return
|
||||
|
||||
syncScope.launch {
|
||||
for ((key, entries) in needsResolution) {
|
||||
val (metaId, metaType) = key
|
||||
val meta = runCatching {
|
||||
MetaDetailsRepository.fetch(metaType, metaId)
|
||||
}.getOrNull() ?: continue
|
||||
|
||||
for (entry in entries) {
|
||||
val episodeVideo = if (entry.seasonNumber != null && entry.episodeNumber != null) {
|
||||
meta.videos.find { v ->
|
||||
v.season == entry.seasonNumber && v.episode == entry.episodeNumber
|
||||
}
|
||||
} else null
|
||||
|
||||
entriesByVideoId[entry.videoId] = entry.copy(
|
||||
title = meta.name,
|
||||
poster = meta.poster,
|
||||
background = meta.background,
|
||||
logo = meta.logo,
|
||||
episodeTitle = episodeVideo?.title ?: entry.episodeTitle,
|
||||
episodeThumbnail = episodeVideo?.thumbnail ?: entry.episodeThumbnail,
|
||||
)
|
||||
}
|
||||
|
||||
publish()
|
||||
}
|
||||
persist()
|
||||
}
|
||||
}
|
||||
|
||||
fun upsertPlaybackProgress(
|
||||
session: WatchProgressPlaybackSession,
|
||||
snapshot: PlayerPlaybackSnapshot,
|
||||
|
|
|
|||
Loading…
Reference in a new issue