mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
feat: trakt library source option to switch between trakt/nuvio library
This commit is contained in:
parent
d00aba86af
commit
55b97d97ad
12 changed files with 381 additions and 93 deletions
|
|
@ -16,6 +16,7 @@ internal actual object PlatformLocalAccountDataCleaner {
|
||||||
"nuvio_mdblist_settings",
|
"nuvio_mdblist_settings",
|
||||||
"nuvio_trakt_auth",
|
"nuvio_trakt_auth",
|
||||||
"nuvio_trakt_library",
|
"nuvio_trakt_library",
|
||||||
|
"nuvio_trakt_settings",
|
||||||
"nuvio_watched",
|
"nuvio_watched",
|
||||||
"nuvio_stream_link_cache",
|
"nuvio_stream_link_cache",
|
||||||
"nuvio_continue_watching_preferences",
|
"nuvio_continue_watching_preferences",
|
||||||
|
|
|
||||||
|
|
@ -783,6 +783,14 @@
|
||||||
<string name="settings_trakt_open_login">Open Trakt Login</string>
|
<string name="settings_trakt_open_login">Open Trakt Login</string>
|
||||||
<string name="settings_trakt_save_actions_description">Your Save actions can now target Trakt watchlist and personal lists.</string>
|
<string name="settings_trakt_save_actions_description">Your Save actions can now target Trakt watchlist and personal lists.</string>
|
||||||
<string name="settings_trakt_sign_in_description">Sign in with Trakt to enable list-based saving and Trakt library mode.</string>
|
<string name="settings_trakt_sign_in_description">Sign in with Trakt to enable list-based saving and Trakt library mode.</string>
|
||||||
|
<string name="trakt_library_source_title">Library Source</string>
|
||||||
|
<string name="trakt_library_source_subtitle">Choose which library to use for saving and viewing your collection</string>
|
||||||
|
<string name="trakt_library_source_dialog_title">Library Source</string>
|
||||||
|
<string name="trakt_library_source_dialog_subtitle">Choose where to save and manage your library items</string>
|
||||||
|
<string name="trakt_library_source_trakt">Trakt</string>
|
||||||
|
<string name="trakt_library_source_nuvio">Nuvio Library</string>
|
||||||
|
<string name="trakt_library_source_trakt_selected">Trakt library selected</string>
|
||||||
|
<string name="trakt_library_source_nuvio_selected">Nuvio library selected</string>
|
||||||
<string name="trakt_watch_progress_title">Watch Progress</string>
|
<string name="trakt_watch_progress_title">Watch Progress</string>
|
||||||
<string name="trakt_watch_progress_subtitle">Choose which progress source powers resume and continue watching</string>
|
<string name="trakt_watch_progress_subtitle">Choose which progress source powers resume and continue watching</string>
|
||||||
<string name="trakt_watch_progress_dialog_title">Watch Progress</string>
|
<string name="trakt_watch_progress_dialog_title">Watch Progress</string>
|
||||||
|
|
|
||||||
|
|
@ -152,8 +152,6 @@ import com.nuvio.app.features.streams.StreamsRepository
|
||||||
import com.nuvio.app.features.streams.StreamsScreen
|
import com.nuvio.app.features.streams.StreamsScreen
|
||||||
import com.nuvio.app.features.tmdb.TmdbService
|
import com.nuvio.app.features.tmdb.TmdbService
|
||||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||||
import com.nuvio.app.features.trakt.TraktAuthRepository
|
|
||||||
import com.nuvio.app.features.trakt.TraktConnectionMode
|
|
||||||
import com.nuvio.app.features.trakt.TraktListTab
|
import com.nuvio.app.features.trakt.TraktListTab
|
||||||
import com.nuvio.app.features.updater.AppUpdaterHost
|
import com.nuvio.app.features.updater.AppUpdaterHost
|
||||||
import com.nuvio.app.features.updater.rememberAppUpdaterController
|
import com.nuvio.app.features.updater.rememberAppUpdaterController
|
||||||
|
|
@ -486,10 +484,6 @@ private fun MainAppContent(
|
||||||
LibraryRepository.ensureLoaded()
|
LibraryRepository.ensureLoaded()
|
||||||
LibraryRepository.uiState
|
LibraryRepository.uiState
|
||||||
}.collectAsStateWithLifecycle()
|
}.collectAsStateWithLifecycle()
|
||||||
val traktAuthUiState by remember {
|
|
||||||
TraktAuthRepository.ensureLoaded()
|
|
||||||
TraktAuthRepository.uiState
|
|
||||||
}.collectAsStateWithLifecycle()
|
|
||||||
val authState by AuthRepository.state.collectAsStateWithLifecycle()
|
val authState by AuthRepository.state.collectAsStateWithLifecycle()
|
||||||
val profileState by ProfileRepository.state.collectAsStateWithLifecycle()
|
val profileState by ProfileRepository.state.collectAsStateWithLifecycle()
|
||||||
val playerSettingsUiState by remember {
|
val playerSettingsUiState by remember {
|
||||||
|
|
@ -508,7 +502,7 @@ private fun MainAppContent(
|
||||||
NetworkStatusRepository.uiState
|
NetworkStatusRepository.uiState
|
||||||
}.collectAsStateWithLifecycle()
|
}.collectAsStateWithLifecycle()
|
||||||
val downloadedProviderLabel = stringResource(Res.string.provider_downloaded)
|
val downloadedProviderLabel = stringResource(Res.string.provider_downloaded)
|
||||||
val isTraktConnected = traktAuthUiState.mode == TraktConnectionMode.CONNECTED
|
val isTraktLibrarySource = libraryUiState.sourceMode == LibrarySourceMode.TRAKT
|
||||||
var initialHomeReady by rememberSaveable { mutableStateOf(false) }
|
var initialHomeReady by rememberSaveable { mutableStateOf(false) }
|
||||||
var offlineLaunchRouteHandled by rememberSaveable { mutableStateOf(false) }
|
var offlineLaunchRouteHandled by rememberSaveable { mutableStateOf(false) }
|
||||||
var networkToastBaselineReady by rememberSaveable { mutableStateOf(false) }
|
var networkToastBaselineReady by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
@ -1664,12 +1658,12 @@ private fun MainAppContent(
|
||||||
onToggleLibrary = {
|
onToggleLibrary = {
|
||||||
selectedPosterForActions?.let { preview ->
|
selectedPosterForActions?.let { preview ->
|
||||||
val libraryItem = preview.toLibraryItem(savedAtEpochMs = 0L)
|
val libraryItem = preview.toLibraryItem(savedAtEpochMs = 0L)
|
||||||
if (!isTraktConnected) {
|
if (!isTraktLibrarySource) {
|
||||||
LibraryRepository.toggleSaved(libraryItem)
|
LibraryRepository.toggleSaved(libraryItem)
|
||||||
} else {
|
} else {
|
||||||
pickerItem = libraryItem
|
pickerItem = libraryItem
|
||||||
pickerTitle = preview.name
|
pickerTitle = preview.name
|
||||||
pickerTabs = LibraryRepository.traktListTabs()
|
pickerTabs = LibraryRepository.libraryListTabs()
|
||||||
pickerMembership = pickerTabs.associate { it.key to false }
|
pickerMembership = pickerTabs.associate { it.key to false }
|
||||||
pickerPending = true
|
pickerPending = true
|
||||||
pickerError = null
|
pickerError = null
|
||||||
|
|
@ -1677,7 +1671,7 @@ private fun MainAppContent(
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
val snapshot = LibraryRepository.getMembershipSnapshot(libraryItem)
|
val snapshot = LibraryRepository.getMembershipSnapshot(libraryItem)
|
||||||
val tabs = LibraryRepository.traktListTabs()
|
val tabs = LibraryRepository.libraryListTabs()
|
||||||
pickerTabs = tabs
|
pickerTabs = tabs
|
||||||
pickerMembership = tabs.associate { tab ->
|
pickerMembership = tabs.associate { tab ->
|
||||||
tab.key to (snapshot[tab.key] == true)
|
tab.key to (snapshot[tab.key] == true)
|
||||||
|
|
|
||||||
|
|
@ -276,39 +276,39 @@ fun MetaDetailsScreen(
|
||||||
val isSaved = remember(
|
val isSaved = remember(
|
||||||
libraryUiState.items,
|
libraryUiState.items,
|
||||||
libraryUiState.sections,
|
libraryUiState.sections,
|
||||||
traktAuthUiState.mode,
|
libraryUiState.sourceMode,
|
||||||
meta.id,
|
meta.id,
|
||||||
meta.type,
|
meta.type,
|
||||||
) {
|
) {
|
||||||
LibraryRepository.isSaved(meta.id, meta.type)
|
LibraryRepository.isSaved(meta.id, meta.type)
|
||||||
}
|
}
|
||||||
val isTraktConnected = traktAuthUiState.mode == TraktConnectionMode.CONNECTED
|
val openLibraryListPicker = remember(meta) {
|
||||||
val toggleSaved = remember(meta, isTraktConnected) {
|
|
||||||
{
|
{
|
||||||
val libraryItem = meta.toLibraryItem(savedAtEpochMs = 0L)
|
val libraryItem = meta.toLibraryItem(savedAtEpochMs = 0L)
|
||||||
if (!isTraktConnected) {
|
pickerTabs = LibraryRepository.libraryListTabs()
|
||||||
LibraryRepository.toggleSaved(libraryItem)
|
pickerMembership = pickerTabs.associate { it.key to false }
|
||||||
} else {
|
pickerPending = true
|
||||||
pickerTabs = LibraryRepository.traktListTabs()
|
pickerError = null
|
||||||
pickerMembership = pickerTabs.associate { it.key to false }
|
showLibraryListPicker = true
|
||||||
pickerPending = true
|
detailsScope.launch {
|
||||||
pickerError = null
|
runCatching {
|
||||||
showLibraryListPicker = true
|
val snapshot = LibraryRepository.getMembershipSnapshot(libraryItem)
|
||||||
detailsScope.launch {
|
val tabs = LibraryRepository.libraryListTabs()
|
||||||
runCatching {
|
pickerTabs = tabs
|
||||||
val snapshot = LibraryRepository.getMembershipSnapshot(libraryItem)
|
pickerMembership = tabs.associate { tab ->
|
||||||
val tabs = LibraryRepository.traktListTabs()
|
tab.key to (snapshot[tab.key] == true)
|
||||||
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
|
}.onFailure { error ->
|
||||||
|
pickerError = error.message ?: getString(Res.string.trakt_lists_load_failed)
|
||||||
}
|
}
|
||||||
Unit
|
pickerPending = false
|
||||||
}
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val toggleSaved = remember(meta) {
|
||||||
|
{
|
||||||
|
LibraryRepository.toggleSaved(meta.toLibraryItem(savedAtEpochMs = 0L))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val movieProgress = watchProgressUiState.byVideoId[meta.id]
|
val movieProgress = watchProgressUiState.byVideoId[meta.id]
|
||||||
|
|
@ -639,6 +639,7 @@ fun MetaDetailsScreen(
|
||||||
onPrimaryPlayClick = onPrimaryPlayClick,
|
onPrimaryPlayClick = onPrimaryPlayClick,
|
||||||
onPrimaryPlayLongClick = onPrimaryPlayLongClick,
|
onPrimaryPlayLongClick = onPrimaryPlayLongClick,
|
||||||
onSaveClick = toggleSaved,
|
onSaveClick = toggleSaved,
|
||||||
|
onSaveLongClick = openLibraryListPicker,
|
||||||
showManualPlayOption = showManualPlayOption,
|
showManualPlayOption = showManualPlayOption,
|
||||||
preferredEpisodeSeasonNumber = seriesAction?.seasonNumber,
|
preferredEpisodeSeasonNumber = seriesAction?.seasonNumber,
|
||||||
preferredEpisodeNumber = seriesAction?.episodeNumber,
|
preferredEpisodeNumber = seriesAction?.episodeNumber,
|
||||||
|
|
@ -946,6 +947,7 @@ private fun ConfiguredMetaSections(
|
||||||
onPrimaryPlayClick: () -> Unit,
|
onPrimaryPlayClick: () -> Unit,
|
||||||
onPrimaryPlayLongClick: (() -> Unit)?,
|
onPrimaryPlayLongClick: (() -> Unit)?,
|
||||||
onSaveClick: () -> Unit,
|
onSaveClick: () -> Unit,
|
||||||
|
onSaveLongClick: (() -> Unit)?,
|
||||||
showManualPlayOption: Boolean,
|
showManualPlayOption: Boolean,
|
||||||
preferredEpisodeSeasonNumber: Int?,
|
preferredEpisodeSeasonNumber: Int?,
|
||||||
preferredEpisodeNumber: Int?,
|
preferredEpisodeNumber: Int?,
|
||||||
|
|
@ -1010,6 +1012,7 @@ private fun ConfiguredMetaSections(
|
||||||
onPlayClick = onPrimaryPlayClick,
|
onPlayClick = onPrimaryPlayClick,
|
||||||
onPlayLongClick = if (showManualPlayOption) onPrimaryPlayLongClick else null,
|
onPlayLongClick = if (showManualPlayOption) onPrimaryPlayLongClick else null,
|
||||||
onSaveClick = onSaveClick,
|
onSaveClick = onSaveClick,
|
||||||
|
onSaveLongClick = onSaveLongClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
MetaScreenSectionKey.OVERVIEW -> {
|
MetaScreenSectionKey.OVERVIEW -> {
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,8 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -44,6 +41,7 @@ fun DetailActionButtons(
|
||||||
onPlayClick: () -> Unit = {},
|
onPlayClick: () -> Unit = {},
|
||||||
onPlayLongClick: (() -> Unit)? = null,
|
onPlayLongClick: (() -> Unit)? = null,
|
||||||
onSaveClick: () -> Unit = {},
|
onSaveClick: () -> Unit = {},
|
||||||
|
onSaveLongClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val playPainter = appIconPainter(AppIconResource.PlayerPlay)
|
val playPainter = appIconPainter(AppIconResource.PlayerPlay)
|
||||||
val libraryAddPainter = appIconPainter(AppIconResource.LibraryAddPlus)
|
val libraryAddPainter = appIconPainter(AppIconResource.LibraryAddPlus)
|
||||||
|
|
@ -96,35 +94,49 @@ fun DetailActionButtons(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedButton(
|
Surface(
|
||||||
onClick = onSaveClick,
|
|
||||||
modifier = rowButtonModifier.height(50.dp),
|
modifier = rowButtonModifier.height(50.dp),
|
||||||
shape = RoundedCornerShape(40.dp),
|
shape = RoundedCornerShape(40.dp),
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0f),
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
) {
|
) {
|
||||||
if (isSaved) {
|
Row(
|
||||||
Icon(
|
modifier = Modifier
|
||||||
imageVector = Icons.Default.Check,
|
.fillMaxWidth()
|
||||||
contentDescription = null,
|
.combinedClickable(
|
||||||
modifier = Modifier.size(20.dp),
|
onClick = onSaveClick,
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
onLongClick = onSaveLongClick,
|
||||||
)
|
role = Role.Button,
|
||||||
} else {
|
)
|
||||||
Icon(
|
.height(50.dp),
|
||||||
painter = libraryAddPainter,
|
horizontalArrangement = Arrangement.Center,
|
||||||
contentDescription = null,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.size(18.dp),
|
) {
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
if (isSaved) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painter = libraryAddPainter,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text(
|
||||||
|
text = saveLabel,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
Text(
|
|
||||||
text = saveLabel,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,20 @@ import com.nuvio.app.core.network.SupabaseProvider
|
||||||
import com.nuvio.app.features.profiles.ProfileRepository
|
import com.nuvio.app.features.profiles.ProfileRepository
|
||||||
import com.nuvio.app.features.trakt.TraktAuthRepository
|
import com.nuvio.app.features.trakt.TraktAuthRepository
|
||||||
import com.nuvio.app.features.trakt.TraktLibraryRepository
|
import com.nuvio.app.features.trakt.TraktLibraryRepository
|
||||||
|
import com.nuvio.app.features.trakt.TraktListTab
|
||||||
|
import com.nuvio.app.features.trakt.TraktListType
|
||||||
import com.nuvio.app.features.trakt.TraktMembershipChanges
|
import com.nuvio.app.features.trakt.TraktMembershipChanges
|
||||||
|
import com.nuvio.app.features.trakt.TraktSettingsRepository
|
||||||
|
import com.nuvio.app.features.trakt.effectiveLibrarySourceMode as resolveEffectiveLibrarySourceMode
|
||||||
|
import com.nuvio.app.features.trakt.shouldUseTraktLibrary
|
||||||
import io.github.jan.supabase.postgrest.postgrest
|
import io.github.jan.supabase.postgrest.postgrest
|
||||||
import io.github.jan.supabase.postgrest.rpc
|
import io.github.jan.supabase.postgrest.rpc
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -65,12 +72,28 @@ object LibraryRepository {
|
||||||
TraktAuthRepository.isAuthenticated.collectLatest { authenticated ->
|
TraktAuthRepository.isAuthenticated.collectLatest { authenticated ->
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
TraktLibraryRepository.preloadListTabsAsync()
|
TraktLibraryRepository.preloadListTabsAsync()
|
||||||
runCatching { TraktLibraryRepository.refreshNow() }
|
if (shouldUseTraktLibrary(authenticated, selectedLibrarySourceMode())) {
|
||||||
.onFailure { log.e(it) { "Failed to refresh Trakt library after auth change" } }
|
runCatching { TraktLibraryRepository.refreshNow() }
|
||||||
|
.onFailure { log.e(it) { "Failed to refresh Trakt library after auth change" } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
publish()
|
publish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
syncScope.launch {
|
||||||
|
TraktSettingsRepository.uiState
|
||||||
|
.map { it.librarySourceMode }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collectLatest { source ->
|
||||||
|
if (shouldUseTraktLibrary(TraktAuthRepository.isAuthenticated.value, source)) {
|
||||||
|
TraktLibraryRepository.preloadListTabsAsync()
|
||||||
|
publish()
|
||||||
|
refreshTraktLibraryAsync()
|
||||||
|
} else {
|
||||||
|
publish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
syncScope.launch {
|
syncScope.launch {
|
||||||
TraktLibraryRepository.uiState.collectLatest {
|
TraktLibraryRepository.uiState.collectLatest {
|
||||||
if (TraktAuthRepository.isAuthenticated.value) {
|
if (TraktAuthRepository.isAuthenticated.value) {
|
||||||
|
|
@ -82,23 +105,29 @@ object LibraryRepository {
|
||||||
|
|
||||||
fun ensureLoaded() {
|
fun ensureLoaded() {
|
||||||
TraktAuthRepository.ensureLoaded()
|
TraktAuthRepository.ensureLoaded()
|
||||||
|
TraktSettingsRepository.ensureLoaded()
|
||||||
TraktLibraryRepository.ensureLoaded()
|
TraktLibraryRepository.ensureLoaded()
|
||||||
if (hasLoaded) return
|
if (hasLoaded) return
|
||||||
loadFromDisk(ProfileRepository.activeProfileId)
|
loadFromDisk(ProfileRepository.activeProfileId)
|
||||||
if (TraktAuthRepository.isAuthenticated.value) {
|
if (TraktAuthRepository.isAuthenticated.value) {
|
||||||
TraktLibraryRepository.preloadListTabsAsync()
|
TraktLibraryRepository.preloadListTabsAsync()
|
||||||
refreshTraktLibraryAsync()
|
if (isTraktLibrarySourceActive()) {
|
||||||
|
refreshTraktLibraryAsync()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onProfileChanged(profileId: Int) {
|
fun onProfileChanged(profileId: Int) {
|
||||||
if (profileId == currentProfileId && hasLoaded) return
|
if (profileId == currentProfileId && hasLoaded) return
|
||||||
|
TraktSettingsRepository.onProfileChanged()
|
||||||
loadFromDisk(profileId)
|
loadFromDisk(profileId)
|
||||||
TraktAuthRepository.onProfileChanged()
|
TraktAuthRepository.onProfileChanged()
|
||||||
TraktLibraryRepository.onProfileChanged()
|
TraktLibraryRepository.onProfileChanged()
|
||||||
if (TraktAuthRepository.isAuthenticated.value) {
|
if (TraktAuthRepository.isAuthenticated.value) {
|
||||||
TraktLibraryRepository.preloadListTabsAsync()
|
TraktLibraryRepository.preloadListTabsAsync()
|
||||||
refreshTraktLibraryAsync()
|
if (isTraktLibrarySourceActive()) {
|
||||||
|
refreshTraktLibraryAsync()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,7 +159,7 @@ object LibraryRepository {
|
||||||
suspend fun pullFromServer(profileId: Int) {
|
suspend fun pullFromServer(profileId: Int) {
|
||||||
currentProfileId = profileId
|
currentProfileId = profileId
|
||||||
|
|
||||||
if (TraktAuthRepository.isAuthenticated.value) {
|
if (isTraktLibrarySourceActive()) {
|
||||||
runCatching { TraktLibraryRepository.refreshNow() }
|
runCatching { TraktLibraryRepository.refreshNow() }
|
||||||
.onFailure { e -> log.e(e) { "Failed to pull Trakt library" } }
|
.onFailure { e -> log.e(e) { "Failed to pull Trakt library" } }
|
||||||
publish()
|
publish()
|
||||||
|
|
@ -157,7 +186,7 @@ object LibraryRepository {
|
||||||
fun toggleSaved(item: LibraryItem) {
|
fun toggleSaved(item: LibraryItem) {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
|
|
||||||
if (TraktAuthRepository.isAuthenticated.value) {
|
if (isTraktLibrarySourceActive()) {
|
||||||
syncScope.launch {
|
syncScope.launch {
|
||||||
runCatching { TraktLibraryRepository.toggleWatchlist(item) }
|
runCatching { TraktLibraryRepository.toggleWatchlist(item) }
|
||||||
.onFailure { e -> log.e(e) { "Failed to toggle Trakt watchlist" } }
|
.onFailure { e -> log.e(e) { "Failed to toggle Trakt watchlist" } }
|
||||||
|
|
@ -175,7 +204,6 @@ object LibraryRepository {
|
||||||
|
|
||||||
fun save(item: LibraryItem) {
|
fun save(item: LibraryItem) {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
if (TraktAuthRepository.isAuthenticated.value) return
|
|
||||||
itemsById[item.id] = item.copy(savedAtEpochMs = LibraryClock.nowEpochMs())
|
itemsById[item.id] = item.copy(savedAtEpochMs = LibraryClock.nowEpochMs())
|
||||||
publish()
|
publish()
|
||||||
persist()
|
persist()
|
||||||
|
|
@ -184,7 +212,6 @@ object LibraryRepository {
|
||||||
|
|
||||||
fun remove(id: String) {
|
fun remove(id: String) {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
if (TraktAuthRepository.isAuthenticated.value) return
|
|
||||||
if (itemsById.remove(id) != null) {
|
if (itemsById.remove(id) != null) {
|
||||||
publish()
|
publish()
|
||||||
persist()
|
persist()
|
||||||
|
|
@ -195,7 +222,7 @@ object LibraryRepository {
|
||||||
fun isSaved(id: String, type: String? = null): Boolean {
|
fun isSaved(id: String, type: String? = null): Boolean {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
|
|
||||||
if (TraktAuthRepository.isAuthenticated.value) {
|
if (isTraktLibrarySourceActive()) {
|
||||||
if (type != null) {
|
if (type != null) {
|
||||||
return TraktLibraryRepository.isInAnyList(id, type)
|
return TraktLibraryRepository.isInAnyList(id, type)
|
||||||
}
|
}
|
||||||
|
|
@ -212,46 +239,65 @@ object LibraryRepository {
|
||||||
fun savedItem(id: String): LibraryItem? {
|
fun savedItem(id: String): LibraryItem? {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
|
|
||||||
if (TraktAuthRepository.isAuthenticated.value) {
|
if (isTraktLibrarySourceActive()) {
|
||||||
return TraktLibraryRepository.uiState.value.allItems.firstOrNull { it.id == id }
|
return TraktLibraryRepository.uiState.value.allItems.firstOrNull { it.id == id }
|
||||||
}
|
}
|
||||||
|
|
||||||
return itemsById[id]
|
return itemsById[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun traktListTabs() = TraktLibraryRepository.currentListTabs()
|
fun libraryListTabs(): List<TraktListTab> {
|
||||||
|
val traktTabs = if (TraktAuthRepository.isAuthenticated.value) {
|
||||||
|
TraktLibraryRepository.currentListTabs()
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
return libraryTabsWithLocal(traktTabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun traktListTabs(): List<TraktListTab> = libraryListTabs()
|
||||||
|
|
||||||
suspend fun getMembershipSnapshot(item: LibraryItem): Map<String, Boolean> {
|
suspend fun getMembershipSnapshot(item: LibraryItem): Map<String, Boolean> {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
if (TraktAuthRepository.isAuthenticated.value) {
|
|
||||||
return TraktLibraryRepository.getMembershipSnapshot(item).listMembership
|
|
||||||
}
|
|
||||||
val inLocal = itemsById.containsKey(item.id)
|
val inLocal = itemsById.containsKey(item.id)
|
||||||
return mapOf(LOCAL_LIST_KEY to inLocal)
|
if (TraktAuthRepository.isAuthenticated.value) {
|
||||||
|
val traktMembership = TraktLibraryRepository.getMembershipSnapshot(item).listMembership
|
||||||
|
return libraryMembershipWithLocal(
|
||||||
|
inLocal = inLocal,
|
||||||
|
traktMembership = traktMembership,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return libraryMembershipWithLocal(inLocal = inLocal)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun applyMembershipChanges(item: LibraryItem, desiredMembership: Map<String, Boolean>) {
|
suspend fun applyMembershipChanges(item: LibraryItem, desiredMembership: Map<String, Boolean>) {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
if (TraktAuthRepository.isAuthenticated.value) {
|
val localDesired = desiredMembership[LOCAL_LIBRARY_LIST_KEY] == true
|
||||||
TraktLibraryRepository.applyMembershipChanges(
|
val currentlyInLocal = itemsById.containsKey(item.id)
|
||||||
item = item,
|
if (localDesired != currentlyInLocal) {
|
||||||
changes = TraktMembershipChanges(desiredMembership = desiredMembership),
|
if (localDesired) {
|
||||||
)
|
save(item)
|
||||||
publish()
|
} else {
|
||||||
return
|
remove(item.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val shouldBeSaved = desiredMembership.values.any { it }
|
if (TraktAuthRepository.isAuthenticated.value) {
|
||||||
if (shouldBeSaved) {
|
val traktMembership = desiredMembership.filterKeys { it != LOCAL_LIBRARY_LIST_KEY }
|
||||||
save(item)
|
if (traktMembership.isNotEmpty()) {
|
||||||
|
TraktLibraryRepository.applyMembershipChanges(
|
||||||
|
item = item,
|
||||||
|
changes = TraktMembershipChanges(desiredMembership = traktMembership),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
publish()
|
||||||
} else {
|
} else {
|
||||||
remove(item.id)
|
publish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pushToServer() {
|
private fun pushToServer() {
|
||||||
syncScope.launch {
|
syncScope.launch {
|
||||||
if (TraktAuthRepository.isAuthenticated.value) return@launch
|
|
||||||
runCatching {
|
runCatching {
|
||||||
val profileId = ProfileRepository.activeProfileId
|
val profileId = ProfileRepository.activeProfileId
|
||||||
val syncItems = itemsById.values.map { it.toSyncItem() }
|
val syncItems = itemsById.values.map { it.toSyncItem() }
|
||||||
|
|
@ -267,7 +313,7 @@ object LibraryRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun publish() {
|
private fun publish() {
|
||||||
if (TraktAuthRepository.isAuthenticated.value) {
|
if (isTraktLibrarySourceActive()) {
|
||||||
val traktState = TraktLibraryRepository.uiState.value
|
val traktState = TraktLibraryRepository.uiState.value
|
||||||
val sections = traktState.listTabs.mapNotNull { tab ->
|
val sections = traktState.listTabs.mapNotNull { tab ->
|
||||||
val listItems = traktState.entriesByList[tab.key].orEmpty()
|
val listItems = traktState.entriesByList[tab.key].orEmpty()
|
||||||
|
|
@ -334,9 +380,42 @@ object LibraryRepository {
|
||||||
publish()
|
publish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun selectedLibrarySourceMode(): LibrarySourceMode {
|
||||||
|
TraktSettingsRepository.ensureLoaded()
|
||||||
|
return TraktSettingsRepository.uiState.value.librarySourceMode
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun effectiveLibrarySourceMode(): LibrarySourceMode =
|
||||||
|
resolveEffectiveLibrarySourceMode(
|
||||||
|
isAuthenticated = TraktAuthRepository.isAuthenticated.value,
|
||||||
|
source = selectedLibrarySourceMode(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun isTraktLibrarySourceActive(): Boolean =
|
||||||
|
effectiveLibrarySourceMode() == LibrarySourceMode.TRAKT
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val LOCAL_LIST_KEY = "local"
|
internal const val LOCAL_LIBRARY_LIST_KEY = "local"
|
||||||
|
internal const val LOCAL_LIBRARY_LIST_TITLE = "Nuvio Library"
|
||||||
|
|
||||||
|
internal fun localLibraryListTab(): TraktListTab =
|
||||||
|
TraktListTab(
|
||||||
|
key = LOCAL_LIBRARY_LIST_KEY,
|
||||||
|
title = LOCAL_LIBRARY_LIST_TITLE,
|
||||||
|
type = TraktListType.WATCHLIST,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun libraryTabsWithLocal(traktTabs: List<TraktListTab>): List<TraktListTab> =
|
||||||
|
listOf(localLibraryListTab()) + traktTabs
|
||||||
|
|
||||||
|
internal fun libraryMembershipWithLocal(
|
||||||
|
inLocal: Boolean,
|
||||||
|
traktMembership: Map<String, Boolean> = emptyMap(),
|
||||||
|
): Map<String, Boolean> =
|
||||||
|
linkedMapOf<String, Boolean>(LOCAL_LIBRARY_LIST_KEY to inLocal).apply {
|
||||||
|
putAll(traktMembership)
|
||||||
|
}
|
||||||
|
|
||||||
private fun LibrarySyncItem.toLibraryItem(): LibraryItem = LibraryItem(
|
private fun LibrarySyncItem.toLibraryItem(): LibraryItem = LibraryItem(
|
||||||
id = contentId,
|
id = contentId,
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,8 @@ object ProfileRepository {
|
||||||
)
|
)
|
||||||
persist()
|
persist()
|
||||||
WatchedRepository.onProfileChanged(profileIndex)
|
WatchedRepository.onProfileChanged(profileIndex)
|
||||||
LibraryRepository.onProfileChanged(profileIndex)
|
|
||||||
TraktSettingsRepository.onProfileChanged()
|
TraktSettingsRepository.onProfileChanged()
|
||||||
|
LibraryRepository.onProfileChanged(profileIndex)
|
||||||
WatchProgressRepository.onProfileChanged(profileIndex)
|
WatchProgressRepository.onProfileChanged(profileIndex)
|
||||||
AddonRepository.onProfileChanged(profileIndex)
|
AddonRepository.onProfileChanged(profileIndex)
|
||||||
if (com.nuvio.app.core.build.AppFeaturePolicy.pluginsEnabled) {
|
if (com.nuvio.app.core.build.AppFeaturePolicy.pluginsEnabled) {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.nuvio.app.features.library.LibrarySourceMode
|
||||||
import com.nuvio.app.features.trakt.TraktAuthRepository
|
import com.nuvio.app.features.trakt.TraktAuthRepository
|
||||||
import com.nuvio.app.features.trakt.TraktBrandAsset
|
import com.nuvio.app.features.trakt.TraktBrandAsset
|
||||||
import com.nuvio.app.features.trakt.TraktAuthUiState
|
import com.nuvio.app.features.trakt.TraktAuthUiState
|
||||||
|
|
@ -73,6 +74,14 @@ import nuvio.composeapp.generated.resources.trakt_continue_watching_window
|
||||||
import nuvio.composeapp.generated.resources.trakt_cw_window_subtitle
|
import nuvio.composeapp.generated.resources.trakt_cw_window_subtitle
|
||||||
import nuvio.composeapp.generated.resources.trakt_cw_window_title
|
import nuvio.composeapp.generated.resources.trakt_cw_window_title
|
||||||
import nuvio.composeapp.generated.resources.trakt_days_format
|
import nuvio.composeapp.generated.resources.trakt_days_format
|
||||||
|
import nuvio.composeapp.generated.resources.trakt_library_source_dialog_subtitle
|
||||||
|
import nuvio.composeapp.generated.resources.trakt_library_source_dialog_title
|
||||||
|
import nuvio.composeapp.generated.resources.trakt_library_source_nuvio
|
||||||
|
import nuvio.composeapp.generated.resources.trakt_library_source_nuvio_selected
|
||||||
|
import nuvio.composeapp.generated.resources.trakt_library_source_subtitle
|
||||||
|
import nuvio.composeapp.generated.resources.trakt_library_source_title
|
||||||
|
import nuvio.composeapp.generated.resources.trakt_library_source_trakt
|
||||||
|
import nuvio.composeapp.generated.resources.trakt_library_source_trakt_selected
|
||||||
import nuvio.composeapp.generated.resources.trakt_watch_progress_dialog_subtitle
|
import nuvio.composeapp.generated.resources.trakt_watch_progress_dialog_subtitle
|
||||||
import nuvio.composeapp.generated.resources.trakt_watch_progress_dialog_title
|
import nuvio.composeapp.generated.resources.trakt_watch_progress_dialog_title
|
||||||
import nuvio.composeapp.generated.resources.trakt_watch_progress_nuvio_selected
|
import nuvio.composeapp.generated.resources.trakt_watch_progress_nuvio_selected
|
||||||
|
|
@ -136,15 +145,27 @@ private fun TraktFeatureRows(
|
||||||
commentsEnabled: Boolean,
|
commentsEnabled: Boolean,
|
||||||
onCommentsEnabledChange: (Boolean) -> Unit,
|
onCommentsEnabledChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
var showLibrarySourceDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
var showWatchProgressDialog by rememberSaveable { mutableStateOf(false) }
|
var showWatchProgressDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
var showContinueWatchingWindowDialog by rememberSaveable { mutableStateOf(false) }
|
var showContinueWatchingWindowDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
var statusMessage by rememberSaveable { mutableStateOf<String?>(null) }
|
var statusMessage by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val librarySourceValue = librarySourceModeLabel(settingsUiState.librarySourceMode)
|
||||||
val watchProgressValue = watchProgressSourceLabel(settingsUiState.watchProgressSource)
|
val watchProgressValue = watchProgressSourceLabel(settingsUiState.watchProgressSource)
|
||||||
val continueWatchingWindowValue = continueWatchingDaysCapLabel(settingsUiState.continueWatchingDaysCap)
|
val continueWatchingWindowValue = continueWatchingDaysCapLabel(settingsUiState.continueWatchingDaysCap)
|
||||||
val traktSelectedMessage = stringResource(Res.string.trakt_watch_progress_trakt_selected)
|
val traktProgressSelectedMessage = stringResource(Res.string.trakt_watch_progress_trakt_selected)
|
||||||
val nuvioSelectedMessage = stringResource(Res.string.trakt_watch_progress_nuvio_selected)
|
val nuvioProgressSelectedMessage = stringResource(Res.string.trakt_watch_progress_nuvio_selected)
|
||||||
|
val traktLibrarySelectedMessage = stringResource(Res.string.trakt_library_source_trakt_selected)
|
||||||
|
val nuvioLibrarySelectedMessage = stringResource(Res.string.trakt_library_source_nuvio_selected)
|
||||||
|
|
||||||
|
TraktSettingsActionRow(
|
||||||
|
title = stringResource(Res.string.trakt_library_source_title),
|
||||||
|
description = stringResource(Res.string.trakt_library_source_subtitle),
|
||||||
|
value = librarySourceValue,
|
||||||
|
isTablet = isTablet,
|
||||||
|
onClick = { showLibrarySourceDialog = true },
|
||||||
|
)
|
||||||
|
SettingsGroupDivider(isTablet = isTablet)
|
||||||
TraktSettingsActionRow(
|
TraktSettingsActionRow(
|
||||||
title = stringResource(Res.string.trakt_watch_progress_title),
|
title = stringResource(Res.string.trakt_watch_progress_title),
|
||||||
description = stringResource(Res.string.trakt_watch_progress_subtitle),
|
description = stringResource(Res.string.trakt_watch_progress_subtitle),
|
||||||
|
|
@ -176,15 +197,31 @@ private fun TraktFeatureRows(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showLibrarySourceDialog) {
|
||||||
|
LibrarySourceModeDialog(
|
||||||
|
selectedSource = settingsUiState.librarySourceMode,
|
||||||
|
onSourceSelected = { source ->
|
||||||
|
TraktSettingsRepository.setLibrarySourceMode(source)
|
||||||
|
statusMessage = if (source == LibrarySourceMode.TRAKT) {
|
||||||
|
traktLibrarySelectedMessage
|
||||||
|
} else {
|
||||||
|
nuvioLibrarySelectedMessage
|
||||||
|
}
|
||||||
|
showLibrarySourceDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showLibrarySourceDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (showWatchProgressDialog) {
|
if (showWatchProgressDialog) {
|
||||||
WatchProgressSourceDialog(
|
WatchProgressSourceDialog(
|
||||||
selectedSource = settingsUiState.watchProgressSource,
|
selectedSource = settingsUiState.watchProgressSource,
|
||||||
onSourceSelected = { source ->
|
onSourceSelected = { source ->
|
||||||
TraktSettingsRepository.setWatchProgressSource(source)
|
TraktSettingsRepository.setWatchProgressSource(source)
|
||||||
statusMessage = if (source == WatchProgressSource.TRAKT) {
|
statusMessage = if (source == WatchProgressSource.TRAKT) {
|
||||||
traktSelectedMessage
|
traktProgressSelectedMessage
|
||||||
} else {
|
} else {
|
||||||
nuvioSelectedMessage
|
nuvioProgressSelectedMessage
|
||||||
}
|
}
|
||||||
showWatchProgressDialog = false
|
showWatchProgressDialog = false
|
||||||
},
|
},
|
||||||
|
|
@ -271,6 +308,13 @@ private fun TraktInfoRow(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun librarySourceModeLabel(source: LibrarySourceMode): String =
|
||||||
|
when (source) {
|
||||||
|
LibrarySourceMode.TRAKT -> stringResource(Res.string.trakt_library_source_trakt)
|
||||||
|
LibrarySourceMode.LOCAL -> stringResource(Res.string.trakt_library_source_nuvio)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun watchProgressSourceLabel(source: WatchProgressSource): String =
|
private fun watchProgressSourceLabel(source: WatchProgressSource): String =
|
||||||
when (source) {
|
when (source) {
|
||||||
|
|
@ -288,6 +332,59 @@ private fun continueWatchingDaysCapLabel(daysCap: Int): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
private fun LibrarySourceModeDialog(
|
||||||
|
selectedSource: LibrarySourceMode,
|
||||||
|
onSourceSelected: (LibrarySourceMode) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.trakt_library_source_dialog_title),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.trakt_library_source_dialog_subtitle),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
listOf(LibrarySourceMode.TRAKT, LibrarySourceMode.LOCAL).forEach { source ->
|
||||||
|
TraktDialogOption(
|
||||||
|
label = librarySourceModeLabel(source),
|
||||||
|
selected = source == selectedSource,
|
||||||
|
onClick = { onSourceSelected(source) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.settings_playback_dialog_close),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
private fun WatchProgressSourceDialog(
|
private fun WatchProgressSourceDialog(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.nuvio.app.features.trakt
|
package com.nuvio.app.features.trakt
|
||||||
|
|
||||||
|
import com.nuvio.app.features.library.LibrarySourceMode
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -35,16 +36,22 @@ enum class WatchProgressSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
val DEFAULT_WATCH_PROGRESS_SOURCE: WatchProgressSource = WatchProgressSource.TRAKT
|
val DEFAULT_WATCH_PROGRESS_SOURCE: WatchProgressSource = WatchProgressSource.TRAKT
|
||||||
|
val DEFAULT_LIBRARY_SOURCE_MODE: LibrarySourceMode = LibrarySourceMode.TRAKT
|
||||||
|
|
||||||
|
fun librarySourceModeFromStorage(value: String?): LibrarySourceMode =
|
||||||
|
LibrarySourceMode.entries.firstOrNull { it.name == value } ?: DEFAULT_LIBRARY_SOURCE_MODE
|
||||||
|
|
||||||
data class TraktSettingsUiState(
|
data class TraktSettingsUiState(
|
||||||
val watchProgressSource: WatchProgressSource = DEFAULT_WATCH_PROGRESS_SOURCE,
|
val watchProgressSource: WatchProgressSource = DEFAULT_WATCH_PROGRESS_SOURCE,
|
||||||
val continueWatchingDaysCap: Int = TRAKT_DEFAULT_CONTINUE_WATCHING_DAYS_CAP,
|
val continueWatchingDaysCap: Int = TRAKT_DEFAULT_CONTINUE_WATCHING_DAYS_CAP,
|
||||||
|
val librarySourceMode: LibrarySourceMode = DEFAULT_LIBRARY_SOURCE_MODE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class StoredTraktSettings(
|
private data class StoredTraktSettings(
|
||||||
val watchProgressSource: String? = null,
|
val watchProgressSource: String? = null,
|
||||||
val continueWatchingDaysCap: Int = TRAKT_DEFAULT_CONTINUE_WATCHING_DAYS_CAP,
|
val continueWatchingDaysCap: Int = TRAKT_DEFAULT_CONTINUE_WATCHING_DAYS_CAP,
|
||||||
|
val librarySourceMode: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
object TraktSettingsRepository {
|
object TraktSettingsRepository {
|
||||||
|
|
@ -87,6 +94,13 @@ object TraktSettingsRepository {
|
||||||
persist()
|
persist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setLibrarySourceMode(mode: LibrarySourceMode) {
|
||||||
|
ensureLoaded()
|
||||||
|
if (_uiState.value.librarySourceMode == mode) return
|
||||||
|
_uiState.value = _uiState.value.copy(librarySourceMode = mode)
|
||||||
|
persist()
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadFromDisk() {
|
private fun loadFromDisk() {
|
||||||
hasLoaded = true
|
hasLoaded = true
|
||||||
|
|
||||||
|
|
@ -104,6 +118,7 @@ object TraktSettingsRepository {
|
||||||
TraktSettingsUiState(
|
TraktSettingsUiState(
|
||||||
watchProgressSource = WatchProgressSource.fromStorage(stored.watchProgressSource),
|
watchProgressSource = WatchProgressSource.fromStorage(stored.watchProgressSource),
|
||||||
continueWatchingDaysCap = normalizeTraktContinueWatchingDaysCap(stored.continueWatchingDaysCap),
|
continueWatchingDaysCap = normalizeTraktContinueWatchingDaysCap(stored.continueWatchingDaysCap),
|
||||||
|
librarySourceMode = librarySourceModeFromStorage(stored.librarySourceMode),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
TraktSettingsUiState()
|
TraktSettingsUiState()
|
||||||
|
|
@ -116,6 +131,7 @@ object TraktSettingsRepository {
|
||||||
StoredTraktSettings(
|
StoredTraktSettings(
|
||||||
watchProgressSource = _uiState.value.watchProgressSource.name,
|
watchProgressSource = _uiState.value.watchProgressSource.name,
|
||||||
continueWatchingDaysCap = _uiState.value.continueWatchingDaysCap,
|
continueWatchingDaysCap = _uiState.value.continueWatchingDaysCap,
|
||||||
|
librarySourceMode = _uiState.value.librarySourceMode.name,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -133,3 +149,18 @@ fun shouldUseTraktProgress(
|
||||||
isAuthenticated: Boolean,
|
isAuthenticated: Boolean,
|
||||||
source: WatchProgressSource,
|
source: WatchProgressSource,
|
||||||
): Boolean = isAuthenticated && source == WatchProgressSource.TRAKT
|
): Boolean = isAuthenticated && source == WatchProgressSource.TRAKT
|
||||||
|
|
||||||
|
fun effectiveLibrarySourceMode(
|
||||||
|
isAuthenticated: Boolean,
|
||||||
|
source: LibrarySourceMode,
|
||||||
|
): LibrarySourceMode =
|
||||||
|
if (isAuthenticated && source == LibrarySourceMode.TRAKT) {
|
||||||
|
LibrarySourceMode.TRAKT
|
||||||
|
} else {
|
||||||
|
LibrarySourceMode.LOCAL
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldUseTraktLibrary(
|
||||||
|
isAuthenticated: Boolean,
|
||||||
|
source: LibrarySourceMode,
|
||||||
|
): Boolean = effectiveLibrarySourceMode(isAuthenticated, source) == LibrarySourceMode.TRAKT
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.nuvio.app.features.library
|
package com.nuvio.app.features.library
|
||||||
|
|
||||||
import com.nuvio.app.features.home.PosterShape
|
import com.nuvio.app.features.home.PosterShape
|
||||||
|
import com.nuvio.app.features.trakt.TraktListTab
|
||||||
|
import com.nuvio.app.features.trakt.TraktListType
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
|
@ -37,4 +39,34 @@ class LibraryRepositoryTest {
|
||||||
assertEquals(PosterShape.Poster, preview.posterShape)
|
assertEquals(PosterShape.Poster, preview.posterShape)
|
||||||
assertEquals("banner", preview.banner)
|
assertEquals("banner", preview.banner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `library tabs include local Nuvio library before Trakt tabs`() {
|
||||||
|
val traktTab = TraktListTab(
|
||||||
|
key = "trakt:watchlist",
|
||||||
|
title = "Watchlist",
|
||||||
|
type = TraktListType.WATCHLIST,
|
||||||
|
)
|
||||||
|
|
||||||
|
val tabs = libraryTabsWithLocal(listOf(traktTab))
|
||||||
|
|
||||||
|
assertEquals(listOf("local", "trakt:watchlist"), tabs.map { it.key })
|
||||||
|
assertEquals("Nuvio Library", tabs.first().title)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `library membership always includes local state before Trakt membership`() {
|
||||||
|
val membership = libraryMembershipWithLocal(
|
||||||
|
inLocal = true,
|
||||||
|
traktMembership = mapOf("trakt:watchlist" to false),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
mapOf(
|
||||||
|
"local" to true,
|
||||||
|
"trakt:watchlist" to false,
|
||||||
|
),
|
||||||
|
membership,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.nuvio.app.features.trakt
|
package com.nuvio.app.features.trakt
|
||||||
|
|
||||||
|
import com.nuvio.app.features.library.LibrarySourceMode
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
|
|
@ -20,6 +21,19 @@ class TraktSettingsRepositoryTest {
|
||||||
assertEquals(WatchProgressSource.NUVIO_SYNC, WatchProgressSource.fromStorage("NUVIO_SYNC"))
|
assertEquals(WatchProgressSource.NUVIO_SYNC, WatchProgressSource.fromStorage("NUVIO_SYNC"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `library source defaults to Trakt for unset or invalid storage`() {
|
||||||
|
assertEquals(LibrarySourceMode.TRAKT, librarySourceModeFromStorage(null))
|
||||||
|
assertEquals(LibrarySourceMode.TRAKT, librarySourceModeFromStorage(""))
|
||||||
|
assertEquals(LibrarySourceMode.TRAKT, librarySourceModeFromStorage("not-a-source"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `library source restores valid storage values`() {
|
||||||
|
assertEquals(LibrarySourceMode.TRAKT, librarySourceModeFromStorage("TRAKT"))
|
||||||
|
assertEquals(LibrarySourceMode.LOCAL, librarySourceModeFromStorage("LOCAL"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `continue watching cap normalizes finite windows and all history`() {
|
fun `continue watching cap normalizes finite windows and all history`() {
|
||||||
assertEquals(TRAKT_CONTINUE_WATCHING_DAYS_CAP_ALL, normalizeTraktContinueWatchingDaysCap(0))
|
assertEquals(TRAKT_CONTINUE_WATCHING_DAYS_CAP_ALL, normalizeTraktContinueWatchingDaysCap(0))
|
||||||
|
|
@ -34,4 +48,20 @@ class TraktSettingsRepositoryTest {
|
||||||
assertFalse(shouldUseTraktProgress(isAuthenticated = true, source = WatchProgressSource.NUVIO_SYNC))
|
assertFalse(shouldUseTraktProgress(isAuthenticated = true, source = WatchProgressSource.NUVIO_SYNC))
|
||||||
assertTrue(shouldUseTraktProgress(isAuthenticated = true, source = WatchProgressSource.TRAKT))
|
assertTrue(shouldUseTraktProgress(isAuthenticated = true, source = WatchProgressSource.TRAKT))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `effective library source uses Trakt only when authenticated and selected`() {
|
||||||
|
assertEquals(
|
||||||
|
LibrarySourceMode.LOCAL,
|
||||||
|
effectiveLibrarySourceMode(isAuthenticated = false, source = LibrarySourceMode.TRAKT),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
LibrarySourceMode.LOCAL,
|
||||||
|
effectiveLibrarySourceMode(isAuthenticated = true, source = LibrarySourceMode.LOCAL),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
LibrarySourceMode.TRAKT,
|
||||||
|
effectiveLibrarySourceMode(isAuthenticated = true, source = LibrarySourceMode.TRAKT),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ internal actual object PlatformLocalAccountDataCleaner {
|
||||||
"mdblist_use_audience",
|
"mdblist_use_audience",
|
||||||
"trakt_auth_payload",
|
"trakt_auth_payload",
|
||||||
"trakt_library_payload",
|
"trakt_library_payload",
|
||||||
|
"trakt_settings_payload",
|
||||||
)
|
)
|
||||||
|
|
||||||
actual fun wipe() {
|
actual fun wipe() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue