mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-04 18:42:34 +00:00
refactor(tracking): register library read providers
This commit is contained in:
parent
5bbe3b9ef0
commit
410a9d4c4b
11 changed files with 372 additions and 302 deletions
|
|
@ -515,6 +515,8 @@
|
|||
<string name="settings_licenses_attributions_exoplayer_license">Licensed under the Apache License, Version 2.0.</string>
|
||||
<string name="compose_trakt_list_picker_loading">Loading your Trakt lists…</string>
|
||||
<string name="compose_trakt_list_picker_subtitle">Choose where to save this title on Trakt</string>
|
||||
<string name="compose_tracking_list_picker_loading">Loading lists…</string>
|
||||
<string name="compose_tracking_list_picker_subtitle">Choose where to save this title</string>
|
||||
<string name="action_donate">Donate</string>
|
||||
<string name="cw_action_go_to_details">Go to details</string>
|
||||
<string name="cw_action_remove">Remove</string>
|
||||
|
|
@ -1442,6 +1444,7 @@
|
|||
<string name="trailer_unable_to_play">Unable to play trailer</string>
|
||||
<string name="trakt_lists_load_failed">Failed to load Trakt lists</string>
|
||||
<string name="trakt_lists_update_failed">Failed to update Trakt lists</string>
|
||||
<string name="tracking_lists_update_failed">Failed to update tracking lists</string>
|
||||
<string name="updates_asset_line">%1$s • %2$s</string>
|
||||
<string name="updates_check_failed">Update check failed</string>
|
||||
<string name="updates_download_failed">Download failed</string>
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ import com.nuvio.app.core.ui.NuvioToastHost
|
|||
import com.nuvio.app.core.ui.NuvioToastController
|
||||
import com.nuvio.app.core.ui.NuvioFloatingPrompt
|
||||
import com.nuvio.app.core.ui.ProfileMeshBackground
|
||||
import com.nuvio.app.core.ui.TraktListPickerDialog
|
||||
import com.nuvio.app.core.ui.TrackingListPickerDialog
|
||||
import com.nuvio.app.core.ui.NuvioTheme
|
||||
import com.nuvio.app.core.ui.NuvioTokens
|
||||
import com.nuvio.app.core.ui.LocalNuvioBottomNavigationOverlayPadding
|
||||
|
|
@ -224,7 +224,7 @@ import com.nuvio.app.features.tracking.TrackingScrobbleAction
|
|||
import com.nuvio.app.features.tracking.TrackingScrobbleCoordinator
|
||||
import com.nuvio.app.features.tracking.TrackingScrobbleEvent
|
||||
import com.nuvio.app.features.tracking.buildTrackingMediaReference
|
||||
import com.nuvio.app.features.trakt.TraktListTab
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryTab
|
||||
import com.nuvio.app.features.updater.AppUpdaterHost
|
||||
import com.nuvio.app.features.updater.AppUpdaterPlatform
|
||||
import com.nuvio.app.features.updater.rememberAppUpdaterController
|
||||
|
|
@ -814,7 +814,7 @@ private fun MainAppContent(
|
|||
var showLibraryListPicker by remember { mutableStateOf(false) }
|
||||
var pickerItem by remember { mutableStateOf<LibraryItem?>(null) }
|
||||
var pickerTitle by remember { mutableStateOf("") }
|
||||
var pickerTabs by remember { mutableStateOf<List<TraktListTab>>(emptyList()) }
|
||||
var pickerTabs by remember { mutableStateOf<List<TrackingLibraryTab>>(emptyList()) }
|
||||
var pickerMembership by remember { mutableStateOf<Map<String, Boolean>>(emptyMap()) }
|
||||
var pickerPending by remember { mutableStateOf(false) }
|
||||
var pickerError by remember { mutableStateOf<String?>(null) }
|
||||
|
|
@ -3341,7 +3341,7 @@ private fun MainAppContent(
|
|||
}
|
||||
}.onFailure { error ->
|
||||
NuvioToastController.show(
|
||||
error.message ?: getString(Res.string.trakt_lists_update_failed),
|
||||
error.message ?: getString(Res.string.tracking_lists_update_failed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3491,7 +3491,7 @@ private fun MainAppContent(
|
|||
},
|
||||
)
|
||||
|
||||
TraktListPickerDialog(
|
||||
TrackingListPickerDialog(
|
||||
visible = showLibraryListPicker,
|
||||
title = pickerTitle,
|
||||
tabs = pickerTabs,
|
||||
|
|
@ -3511,7 +3511,7 @@ private fun MainAppContent(
|
|||
}
|
||||
},
|
||||
onSave = {
|
||||
val item = pickerItem ?: return@TraktListPickerDialog
|
||||
val item = pickerItem ?: return@TrackingListPickerDialog
|
||||
coroutineScope.launch {
|
||||
pickerPending = true
|
||||
pickerError = null
|
||||
|
|
@ -3525,7 +3525,7 @@ private fun MainAppContent(
|
|||
pickerItem = null
|
||||
pickerError = null
|
||||
}.onFailure { error ->
|
||||
pickerError = error.message ?: getString(Res.string.trakt_lists_update_failed)
|
||||
pickerError = error.message ?: getString(Res.string.tracking_lists_update_failed)
|
||||
}
|
||||
pickerPending = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import com.nuvio.app.features.simkl.SimklAuthRepository
|
|||
import com.nuvio.app.features.simkl.SimklMutationRepository
|
||||
import com.nuvio.app.features.simkl.SimklLibraryRepository
|
||||
import com.nuvio.app.features.simkl.SimklProgressRepository
|
||||
import com.nuvio.app.features.simkl.SimklTrackingLibraryProvider
|
||||
import com.nuvio.app.features.simkl.SimklSyncRepository
|
||||
import com.nuvio.app.features.tracking.TrackingProviderRegistry
|
||||
import com.nuvio.app.features.trakt.TraktAuthRepository
|
||||
import com.nuvio.app.features.trakt.TraktScrobbleRepository
|
||||
import com.nuvio.app.features.trakt.TraktTrackingLibraryProvider
|
||||
|
||||
fun ensureTrackingProvidersRegistered() {
|
||||
TraktAuthRepository.descriptor
|
||||
|
|
@ -16,4 +19,6 @@ fun ensureTrackingProvidersRegistered() {
|
|||
SimklLibraryRepository.uiState
|
||||
SimklProgressRepository.uiState
|
||||
SimklMutationRepository.ensureRegistered()
|
||||
TrackingProviderRegistry.registerLibraryProvider(TraktTrackingLibraryProvider)
|
||||
TrackingProviderRegistry.registerLibraryProvider(SimklTrackingLibraryProvider)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,20 +25,20 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.nuvio.app.features.trakt.TraktListTab
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryTab
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.action_cancel
|
||||
import nuvio.composeapp.generated.resources.action_save
|
||||
import nuvio.composeapp.generated.resources.compose_trakt_list_picker_loading
|
||||
import nuvio.composeapp.generated.resources.compose_trakt_list_picker_subtitle
|
||||
import nuvio.composeapp.generated.resources.compose_tracking_list_picker_loading
|
||||
import nuvio.composeapp.generated.resources.compose_tracking_list_picker_subtitle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TraktListPickerDialog(
|
||||
fun TrackingListPickerDialog(
|
||||
visible: Boolean,
|
||||
title: String,
|
||||
tabs: List<TraktListTab>,
|
||||
tabs: List<TrackingLibraryTab>,
|
||||
membership: Map<String, Boolean>,
|
||||
isPending: Boolean,
|
||||
errorMessage: String?,
|
||||
|
|
@ -67,7 +67,7 @@ fun TraktListPickerDialog(
|
|||
color = tokens.colors.textPrimary,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.compose_trakt_list_picker_subtitle),
|
||||
text = stringResource(Res.string.compose_tracking_list_picker_subtitle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = tokens.colors.textMuted,
|
||||
)
|
||||
|
|
@ -95,7 +95,7 @@ fun TraktListPickerDialog(
|
|||
modifier = Modifier.size(tokens.icons.lg),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.compose_trakt_list_picker_loading),
|
||||
text = stringResource(Res.string.compose_tracking_list_picker_loading),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = tokens.colors.textMuted,
|
||||
)
|
||||
|
|
@ -82,7 +82,7 @@ import com.nuvio.app.core.ui.NuvioPosterZoomActionOverlay
|
|||
import com.nuvio.app.core.ui.PosterZoomAnchor
|
||||
import com.nuvio.app.core.ui.PosterZoomAnchorHolder
|
||||
import com.nuvio.app.core.ui.PosterZoomOverlayAction
|
||||
import com.nuvio.app.core.ui.TraktListPickerDialog
|
||||
import com.nuvio.app.core.ui.TrackingListPickerDialog
|
||||
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
|
||||
import com.nuvio.app.core.ui.rememberHeroStretchState
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
|
|
@ -115,7 +115,7 @@ import com.nuvio.app.features.trakt.TraktCommentReview
|
|||
import com.nuvio.app.features.trakt.TraktCommentsRepository
|
||||
import com.nuvio.app.features.trakt.TraktCommentsSettings
|
||||
import com.nuvio.app.features.trakt.TraktConnectionMode
|
||||
import com.nuvio.app.features.trakt.TraktListTab
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryTab
|
||||
import com.nuvio.app.features.trakt.TraktSettingsRepository
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import com.nuvio.app.features.trailer.TrailerPlaybackResolver
|
||||
|
|
@ -213,7 +213,7 @@ fun MetaDetailsScreen(
|
|||
var selectedComment by remember(type, id) { mutableStateOf<TraktCommentReview?>(null) }
|
||||
val detailsScope = rememberCoroutineScope()
|
||||
var showLibraryListPicker by remember(type, id) { mutableStateOf(false) }
|
||||
var pickerTabs by remember(type, id) { mutableStateOf<List<TraktListTab>>(emptyList()) }
|
||||
var pickerTabs by remember(type, id) { mutableStateOf<List<TrackingLibraryTab>>(emptyList()) }
|
||||
var pickerMembership by remember(type, id) { mutableStateOf<Map<String, Boolean>>(emptyMap()) }
|
||||
var pickerPending by remember(type, id) { mutableStateOf(false) }
|
||||
var pickerError by remember(type, id) { mutableStateOf<String?>(null) }
|
||||
|
|
@ -1212,7 +1212,7 @@ fun MetaDetailsScreen(
|
|||
)
|
||||
}
|
||||
|
||||
TraktListPickerDialog(
|
||||
TrackingListPickerDialog(
|
||||
visible = showLibraryListPicker,
|
||||
title = meta.name,
|
||||
tabs = pickerTabs,
|
||||
|
|
@ -1241,7 +1241,7 @@ fun MetaDetailsScreen(
|
|||
}.onSuccess {
|
||||
showLibraryListPicker = false
|
||||
}.onFailure { error ->
|
||||
pickerError = error.message ?: getString(Res.string.trakt_lists_update_failed)
|
||||
pickerError = error.message ?: getString(Res.string.tracking_lists_update_failed)
|
||||
}
|
||||
pickerPending = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,18 +9,13 @@ import com.nuvio.app.core.tracking.ensureTrackingProvidersRegistered
|
|||
import com.nuvio.app.core.ui.NuvioToastController
|
||||
import com.nuvio.app.features.home.PosterShape
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
import com.nuvio.app.features.simkl.SIMKL_WATCHLIST_KEY
|
||||
import com.nuvio.app.features.simkl.SIMKL_WATCHLIST_TITLE
|
||||
import com.nuvio.app.features.simkl.SimklLibraryRepository
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryProvider
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryTab
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryTabKind
|
||||
import com.nuvio.app.features.tracking.TrackingProviderRegistry
|
||||
import com.nuvio.app.features.tracking.TrackingSettingsRepository
|
||||
import com.nuvio.app.features.tracking.effectiveLibrarySourceMode as resolveEffectiveLibrarySourceMode
|
||||
import com.nuvio.app.features.trakt.TraktAuthRepository
|
||||
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.TraktSettingsRepository
|
||||
import com.nuvio.app.features.tracking.providerId
|
||||
import io.github.jan.supabase.postgrest.postgrest
|
||||
import io.github.jan.supabase.postgrest.rpc
|
||||
import kotlinx.atomicfu.locks.SynchronizedObject
|
||||
|
|
@ -52,7 +47,7 @@ import kotlinx.serialization.json.put
|
|||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.library_local_tab_title
|
||||
import nuvio.composeapp.generated.resources.library_other
|
||||
import nuvio.composeapp.generated.resources.trakt_lists_update_failed
|
||||
import nuvio.composeapp.generated.resources.tracking_lists_update_failed
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
|
|
@ -97,55 +92,34 @@ object LibraryRepository {
|
|||
private val lastPersistedContentRevisionByProfile = mutableMapOf<Int, Long>()
|
||||
|
||||
init {
|
||||
ensureTrackingProvidersRegistered()
|
||||
syncScope.launch {
|
||||
TrackingProviderRegistry.connectedProviderIds.collectLatest { connectedProviderIds ->
|
||||
if (TrackingProviderId.TRAKT in connectedProviderIds) {
|
||||
TraktLibraryRepository.preloadListTabsAsync()
|
||||
if (effectiveLibrarySourceMode() == LibrarySourceMode.TRAKT) {
|
||||
runCatching { TraktLibraryRepository.refreshNow() }
|
||||
.onFailure { log.e(it) { "Failed to refresh Trakt library after auth change" } }
|
||||
}
|
||||
}
|
||||
if (
|
||||
TrackingProviderId.SIMKL in connectedProviderIds &&
|
||||
effectiveLibrarySourceMode() == LibrarySourceMode.SIMKL
|
||||
) {
|
||||
runCatching { SimklLibraryRepository.refreshNow() }
|
||||
.onFailure { log.e(it) { "Failed to refresh Simkl library after auth change" } }
|
||||
TrackingProviderRegistry.connectedProviderIds.collectLatest {
|
||||
TrackingProviderRegistry.connectedLibraryProviders().forEach(TrackingLibraryProvider::prepare)
|
||||
activeLibraryProvider()?.let { provider ->
|
||||
refreshLibraryProvider(provider, "authentication change")
|
||||
}
|
||||
publish()
|
||||
}
|
||||
}
|
||||
syncScope.launch {
|
||||
TraktSettingsRepository.uiState
|
||||
TrackingSettingsRepository.uiState
|
||||
.map { it.librarySourceMode }
|
||||
.distinctUntilChanged()
|
||||
.collectLatest { source ->
|
||||
when (resolveEffectiveLibrarySourceMode(source, TrackingProviderRegistry::isAuthenticated)) {
|
||||
LibrarySourceMode.TRAKT -> {
|
||||
TraktLibraryRepository.preloadListTabsAsync()
|
||||
publish()
|
||||
refreshTraktLibraryAsync()
|
||||
}
|
||||
LibrarySourceMode.SIMKL -> {
|
||||
publish()
|
||||
refreshSimklLibraryAsync()
|
||||
}
|
||||
LibrarySourceMode.LOCAL -> publish()
|
||||
.collectLatest {
|
||||
publish()
|
||||
activeLibraryProvider()?.let { provider ->
|
||||
provider.prepare()
|
||||
refreshLibraryProviderAsync(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
syncScope.launch {
|
||||
TraktLibraryRepository.uiState.collectLatest {
|
||||
if (TraktAuthRepository.isAuthenticated.value) {
|
||||
publish()
|
||||
}
|
||||
}
|
||||
}
|
||||
syncScope.launch {
|
||||
SimklLibraryRepository.uiState.collectLatest {
|
||||
if (TrackingProviderRegistry.isAuthenticated(TrackingProviderId.SIMKL)) {
|
||||
publish()
|
||||
TrackingProviderRegistry.libraryProviders().forEach { provider ->
|
||||
syncScope.launch {
|
||||
provider.changes.collectLatest {
|
||||
if (TrackingProviderRegistry.isAuthenticated(provider.providerId)) {
|
||||
publish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -154,47 +128,32 @@ object LibraryRepository {
|
|||
fun ensureLoaded() {
|
||||
ensureTrackingProvidersRegistered()
|
||||
TrackingProviderRegistry.ensureLoaded()
|
||||
TraktSettingsRepository.ensureLoaded()
|
||||
TraktLibraryRepository.ensureLoaded()
|
||||
SimklLibraryRepository.ensureLoaded()
|
||||
TrackingSettingsRepository.ensureLoaded()
|
||||
TrackingProviderRegistry.libraryProviders().forEach(TrackingLibraryProvider::ensureLoaded)
|
||||
while (true) {
|
||||
val activeProfileId = ProfileRepository.activeProfileId
|
||||
val snapshot = localState.snapshot()
|
||||
if (snapshot.hasLoaded && snapshot.token.profileId == activeProfileId) break
|
||||
loadFromDisk(activeProfileId)
|
||||
}
|
||||
if (TrackingProviderRegistry.isAuthenticated(TrackingProviderId.TRAKT)) {
|
||||
TraktLibraryRepository.preloadListTabsAsync()
|
||||
if (isTraktLibrarySourceActive()) {
|
||||
refreshTraktLibraryAsync()
|
||||
}
|
||||
}
|
||||
if (isSimklLibrarySourceActive()) refreshSimklLibraryAsync()
|
||||
TrackingProviderRegistry.connectedLibraryProviders().forEach(TrackingLibraryProvider::prepare)
|
||||
activeLibraryProvider()?.let(::refreshLibraryProviderAsync)
|
||||
}
|
||||
|
||||
fun onProfileChanged(profileId: Int) {
|
||||
val current = localState.snapshot()
|
||||
if (profileId == current.token.profileId && current.hasLoaded) return
|
||||
|
||||
TraktSettingsRepository.onProfileChanged()
|
||||
if (!loadFromDisk(profileId)) return
|
||||
TraktAuthRepository.onProfileChanged()
|
||||
TraktLibraryRepository.onProfileChanged()
|
||||
if (TraktAuthRepository.isAuthenticated.value) {
|
||||
TraktLibraryRepository.preloadListTabsAsync()
|
||||
if (isTraktLibrarySourceActive()) {
|
||||
refreshTraktLibraryAsync()
|
||||
}
|
||||
}
|
||||
SimklLibraryRepository.ensureLoaded()
|
||||
if (isSimklLibrarySourceActive()) refreshSimklLibraryAsync()
|
||||
TrackingProviderRegistry.libraryProviders().forEach(TrackingLibraryProvider::onProfileChanged)
|
||||
TrackingProviderRegistry.connectedLibraryProviders().forEach(TrackingLibraryProvider::prepare)
|
||||
activeLibraryProvider()?.let(::refreshLibraryProviderAsync)
|
||||
}
|
||||
|
||||
fun clearLocalState() {
|
||||
val transition = synchronized(loadLock) { localState.reset() }
|
||||
transition.detachedPushJob?.cancel()
|
||||
TraktAuthRepository.clearLocalState()
|
||||
TraktLibraryRepository.clearLocalState()
|
||||
TrackingProviderRegistry.libraryProviders().forEach(TrackingLibraryProvider::clearLocalState)
|
||||
_uiState.value = LibraryUiState()
|
||||
}
|
||||
|
||||
|
|
@ -253,32 +212,11 @@ object LibraryRepository {
|
|||
return
|
||||
}
|
||||
|
||||
when (effectiveLibrarySourceMode()) {
|
||||
LibrarySourceMode.TRAKT -> {
|
||||
try {
|
||||
TraktLibraryRepository.refreshNow()
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
log.e(error) { "Failed to pull Trakt library" }
|
||||
}
|
||||
if (!isActiveOperation(operationToken)) return
|
||||
publish()
|
||||
return
|
||||
}
|
||||
LibrarySourceMode.SIMKL -> {
|
||||
try {
|
||||
SimklLibraryRepository.refreshNow()
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
log.e(error) { "Failed to pull Simkl library" }
|
||||
}
|
||||
if (!isActiveOperation(operationToken)) return
|
||||
publish()
|
||||
return
|
||||
}
|
||||
LibrarySourceMode.LOCAL -> Unit
|
||||
activeLibraryProvider()?.let { provider ->
|
||||
refreshLibraryProvider(provider, "explicit pull")
|
||||
if (!isActiveOperation(operationToken)) return
|
||||
publish()
|
||||
return
|
||||
}
|
||||
|
||||
nuvioPullMutex.withLock {
|
||||
|
|
@ -322,16 +260,20 @@ object LibraryRepository {
|
|||
fun toggleSaved(item: LibraryItem) {
|
||||
ensureLoaded()
|
||||
|
||||
if (isTraktLibrarySourceActive()) {
|
||||
activeLibraryProvider()?.let { provider ->
|
||||
val profileId = localState.snapshot().token.profileId
|
||||
log.i { "toggleSaved routed to Trakt library source item=${item.id} type=${item.type} profile=$profileId" }
|
||||
log.i {
|
||||
"toggleSaved routed to ${provider.providerId.storageId} library source " +
|
||||
"item=${item.id} type=${item.type} profile=$profileId"
|
||||
}
|
||||
syncScope.launch {
|
||||
runCatching { TraktLibraryRepository.toggleWatchlist(item) }
|
||||
.onFailure { e ->
|
||||
log.e(e) { "Failed to toggle Trakt watchlist" }
|
||||
runCatching { provider.toggleDefaultMembership(profileId, item) }
|
||||
.onFailure { error ->
|
||||
if (error is CancellationException) throw error
|
||||
log.e(error) { "Failed to toggle ${provider.providerId.storageId} default library membership" }
|
||||
NuvioToastController.show(
|
||||
e.message?.takeIf { it.isNotBlank() }
|
||||
?: getString(Res.string.trakt_lists_update_failed),
|
||||
error.message?.takeIf(String::isNotBlank)
|
||||
?: getString(Res.string.tracking_lists_update_failed),
|
||||
)
|
||||
}
|
||||
publish()
|
||||
|
|
@ -339,29 +281,6 @@ object LibraryRepository {
|
|||
return
|
||||
}
|
||||
|
||||
if (isSimklLibrarySourceActive()) {
|
||||
val profileId = localState.snapshot().token.profileId
|
||||
val isCurrentlySaved = SimklLibraryRepository.isInWatchlist(item.id, item.type)
|
||||
log.i {
|
||||
"toggleSaved routed to Simkl library source item=${item.id} type=${item.type} profile=$profileId"
|
||||
}
|
||||
syncScope.launch {
|
||||
runCatching {
|
||||
SimklLibraryRepository.setWatchlistMembership(
|
||||
profileId = profileId,
|
||||
item = item,
|
||||
isMember = !isCurrentlySaved,
|
||||
)
|
||||
}.onFailure { error ->
|
||||
if (error is CancellationException) throw error
|
||||
log.e(error) { "Failed to toggle Simkl watchlist" }
|
||||
NuvioToastController.show(error.message ?: "Unable to update Simkl Watchlist")
|
||||
}
|
||||
publish()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val result = localState.toggle(
|
||||
item.copy(savedAtEpochMs = LibraryClock.nowEpochMs()),
|
||||
)
|
||||
|
|
@ -422,20 +341,7 @@ object LibraryRepository {
|
|||
fun isSaved(id: String, type: String? = null): Boolean {
|
||||
ensureLoaded()
|
||||
|
||||
if (isTraktLibrarySourceActive()) {
|
||||
if (type != null) {
|
||||
return TraktLibraryRepository.isInAnyList(id, type)
|
||||
}
|
||||
val entry = TraktLibraryRepository.uiState.value.allItems.firstOrNull { it.id == id }
|
||||
if (entry != null) {
|
||||
return TraktLibraryRepository.isInAnyList(entry.id, entry.type)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (isSimklLibrarySourceActive()) {
|
||||
return SimklLibraryRepository.isInWatchlist(id, type)
|
||||
}
|
||||
activeLibraryProvider()?.let { provider -> return provider.contains(id, type) }
|
||||
|
||||
return if (type != null) {
|
||||
localState.contains(id, type)
|
||||
|
|
@ -447,44 +353,25 @@ object LibraryRepository {
|
|||
fun savedItem(id: String): LibraryItem? {
|
||||
ensureLoaded()
|
||||
|
||||
if (isTraktLibrarySourceActive()) {
|
||||
return TraktLibraryRepository.uiState.value.allItems.firstOrNull { it.id == id }
|
||||
}
|
||||
|
||||
if (isSimklLibrarySourceActive()) {
|
||||
return SimklLibraryRepository.uiState.value.items.firstOrNull { it.id == id }
|
||||
}
|
||||
activeLibraryProvider()?.let { provider -> return provider.find(id) }
|
||||
|
||||
return localState.findById(id)
|
||||
}
|
||||
|
||||
fun libraryListTabs(): List<TraktListTab> {
|
||||
val traktTabs = if (TrackingProviderRegistry.isAuthenticated(TrackingProviderId.TRAKT)) {
|
||||
TraktLibraryRepository.currentListTabs()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val simklTabs = if (TrackingProviderRegistry.isAuthenticated(TrackingProviderId.SIMKL)) {
|
||||
listOf(simklLibraryListTab())
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
return libraryTabsWithLocal(traktTabs + simklTabs)
|
||||
}
|
||||
|
||||
fun traktListTabs(): List<TraktListTab> = libraryListTabs()
|
||||
fun libraryListTabs(): List<TrackingLibraryTab> =
|
||||
libraryTabsWithLocal(
|
||||
TrackingProviderRegistry.connectedLibraryProviders()
|
||||
.flatMap { provider -> provider.snapshot().tabs },
|
||||
)
|
||||
|
||||
suspend fun getMembershipSnapshot(item: LibraryItem): Map<String, Boolean> {
|
||||
ensureLoaded()
|
||||
val inLocal = localState.contains(item.id, item.type)
|
||||
val memberships = linkedMapOf<String, Boolean>()
|
||||
if (TrackingProviderRegistry.isAuthenticated(TrackingProviderId.TRAKT)) {
|
||||
memberships += TraktLibraryRepository.getMembershipSnapshot(item).listMembership
|
||||
TrackingProviderRegistry.connectedLibraryProviders().forEach { provider ->
|
||||
memberships += provider.membership(item)
|
||||
}
|
||||
if (TrackingProviderRegistry.isAuthenticated(TrackingProviderId.SIMKL)) {
|
||||
memberships[SIMKL_WATCHLIST_KEY] = SimklLibraryRepository.isInWatchlist(item.id, item.type)
|
||||
}
|
||||
return libraryMembershipWithLocal(inLocal = inLocal, traktMembership = memberships)
|
||||
return libraryMembershipWithLocal(inLocal = inLocal, providerMembership = memberships)
|
||||
}
|
||||
|
||||
suspend fun applyMembershipChanges(item: LibraryItem, desiredMembership: Map<String, Boolean>) {
|
||||
|
|
@ -506,38 +393,21 @@ object LibraryRepository {
|
|||
}
|
||||
|
||||
var firstFailure: Throwable? = null
|
||||
if (TrackingProviderRegistry.isAuthenticated(TrackingProviderId.TRAKT)) {
|
||||
val traktListKeys = TraktLibraryRepository.currentListTabs().mapTo(mutableSetOf(), TraktListTab::key)
|
||||
val traktMembership = desiredMembership.filterKeys { key -> key in traktListKeys }
|
||||
if (traktMembership.isNotEmpty()) {
|
||||
TrackingProviderRegistry.connectedLibraryProviders().forEach { provider ->
|
||||
val providerListKeys = provider.snapshot().tabs.mapTo(mutableSetOf(), TrackingLibraryTab::key)
|
||||
val providerMembership = desiredMembership.filterKeys(providerListKeys::contains)
|
||||
if (providerMembership.isNotEmpty()) {
|
||||
try {
|
||||
TraktLibraryRepository.applyMembershipChanges(
|
||||
item = item,
|
||||
changes = TraktMembershipChanges(desiredMembership = traktMembership),
|
||||
)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
firstFailure = error
|
||||
log.e(error) { "Failed to update Trakt library membership" }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (TrackingProviderRegistry.isAuthenticated(TrackingProviderId.SIMKL)) {
|
||||
val desired = desiredMembership[SIMKL_WATCHLIST_KEY] == true
|
||||
val current = SimklLibraryRepository.isInWatchlist(item.id, item.type)
|
||||
if (desired != current) {
|
||||
try {
|
||||
SimklLibraryRepository.setWatchlistMembership(
|
||||
provider.applyMembership(
|
||||
profileId = profileId,
|
||||
item = item,
|
||||
isMember = desired,
|
||||
desiredMembership = providerMembership,
|
||||
)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
if (firstFailure == null) firstFailure = error
|
||||
log.e(error) { "Failed to update Simkl library membership" }
|
||||
log.e(error) { "Failed to update ${provider.providerId.storageId} library membership" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -634,59 +504,21 @@ object LibraryRepository {
|
|||
|
||||
private fun publish() {
|
||||
val localSnapshot = localState.snapshot()
|
||||
when (effectiveLibrarySourceMode()) {
|
||||
LibrarySourceMode.TRAKT -> {
|
||||
val traktState = TraktLibraryRepository.uiState.value
|
||||
val sections = traktState.listTabs.mapNotNull { tab ->
|
||||
val listItems = traktState.entriesByList[tab.key].orEmpty()
|
||||
if (listItems.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
LibrarySection(
|
||||
type = tab.key,
|
||||
displayTitle = tab.title,
|
||||
items = listItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val newUiState = LibraryUiState(
|
||||
sourceMode = LibrarySourceMode.TRAKT,
|
||||
items = traktState.allItems,
|
||||
sections = sections,
|
||||
isLoaded = traktState.hasLoaded,
|
||||
isLoading = traktState.isLoading,
|
||||
errorMessage = traktState.errorMessage,
|
||||
)
|
||||
localState.runIfTokenCurrent(localSnapshot.token) {
|
||||
_uiState.value = newUiState
|
||||
}
|
||||
return
|
||||
val sourceMode = effectiveLibrarySourceMode()
|
||||
activeLibraryProvider(sourceMode)?.let { provider ->
|
||||
val providerSnapshot = provider.snapshot()
|
||||
val newUiState = LibraryUiState(
|
||||
sourceMode = sourceMode,
|
||||
items = providerSnapshot.items,
|
||||
sections = providerSnapshot.sections,
|
||||
isLoaded = providerSnapshot.hasLoaded,
|
||||
isLoading = providerSnapshot.isLoading,
|
||||
errorMessage = providerSnapshot.errorMessage,
|
||||
)
|
||||
localState.runIfTokenCurrent(localSnapshot.token) {
|
||||
_uiState.value = newUiState
|
||||
}
|
||||
LibrarySourceMode.SIMKL -> {
|
||||
val simklState = SimklLibraryRepository.uiState.value
|
||||
val items = simklState.items.sortedByDescending(LibraryItem::savedAtEpochMs)
|
||||
val sections = listOf(
|
||||
LibrarySection(
|
||||
type = SIMKL_WATCHLIST_KEY,
|
||||
displayTitle = SIMKL_WATCHLIST_TITLE,
|
||||
items = items,
|
||||
),
|
||||
)
|
||||
val newUiState = LibraryUiState(
|
||||
sourceMode = LibrarySourceMode.SIMKL,
|
||||
items = items,
|
||||
sections = sections,
|
||||
isLoaded = simklState.hasLoaded,
|
||||
isLoading = simklState.isLoading,
|
||||
errorMessage = simklState.errorMessage,
|
||||
)
|
||||
localState.runIfTokenCurrent(localSnapshot.token) {
|
||||
_uiState.value = newUiState
|
||||
}
|
||||
return
|
||||
}
|
||||
LibrarySourceMode.LOCAL -> Unit
|
||||
return
|
||||
}
|
||||
|
||||
val items = localSnapshot.items
|
||||
|
|
@ -732,28 +564,31 @@ object LibraryRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private fun refreshTraktLibraryAsync() {
|
||||
private fun refreshLibraryProviderAsync(provider: TrackingLibraryProvider) {
|
||||
syncScope.launch {
|
||||
runCatching { TraktLibraryRepository.refreshNow() }
|
||||
.onFailure { e -> log.e(e) { "Failed to refresh Trakt library" } }
|
||||
refreshLibraryProvider(provider, "background refresh")
|
||||
publish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshSimklLibraryAsync() {
|
||||
syncScope.launch {
|
||||
runCatching { SimklLibraryRepository.refreshNow() }
|
||||
.onFailure { error ->
|
||||
if (error is CancellationException) throw error
|
||||
log.e(error) { "Failed to refresh Simkl library" }
|
||||
}
|
||||
publish()
|
||||
private suspend fun refreshLibraryProvider(
|
||||
provider: TrackingLibraryProvider,
|
||||
reason: String,
|
||||
) {
|
||||
try {
|
||||
provider.refresh()
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
log.e(error) {
|
||||
"Failed to refresh ${provider.providerId.storageId} library during $reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectedLibrarySourceMode(): LibrarySourceMode {
|
||||
TraktSettingsRepository.ensureLoaded()
|
||||
return TraktSettingsRepository.uiState.value.librarySourceMode
|
||||
TrackingSettingsRepository.ensureLoaded()
|
||||
return TrackingSettingsRepository.uiState.value.librarySourceMode
|
||||
}
|
||||
|
||||
private fun effectiveLibrarySourceMode(): LibrarySourceMode =
|
||||
|
|
@ -762,43 +597,36 @@ object LibraryRepository {
|
|||
isProviderAuthenticated = TrackingProviderRegistry::isAuthenticated,
|
||||
)
|
||||
|
||||
private fun isTraktLibrarySourceActive(): Boolean =
|
||||
effectiveLibrarySourceMode() == LibrarySourceMode.TRAKT
|
||||
|
||||
private fun isSimklLibrarySourceActive(): Boolean =
|
||||
effectiveLibrarySourceMode() == LibrarySourceMode.SIMKL
|
||||
private fun activeLibraryProvider(
|
||||
sourceMode: LibrarySourceMode = effectiveLibrarySourceMode(),
|
||||
): TrackingLibraryProvider? =
|
||||
sourceMode.providerId?.let(TrackingProviderRegistry::libraryProvider)
|
||||
}
|
||||
|
||||
internal const val LOCAL_LIBRARY_LIST_KEY = "local"
|
||||
private const val DEFAULT_LOCAL_LIBRARY_TAB_TITLE = "Nuvio Library"
|
||||
private const val DEFAULT_LIBRARY_OTHER_TITLE = "Other"
|
||||
|
||||
internal fun localLibraryListTab(): TraktListTab =
|
||||
TraktListTab(
|
||||
internal fun localLibraryListTab(): TrackingLibraryTab =
|
||||
TrackingLibraryTab(
|
||||
key = LOCAL_LIBRARY_LIST_KEY,
|
||||
title = localizedStringOrDefault(
|
||||
resource = Res.string.library_local_tab_title,
|
||||
fallback = DEFAULT_LOCAL_LIBRARY_TAB_TITLE,
|
||||
),
|
||||
type = TraktListType.WATCHLIST,
|
||||
providerId = null,
|
||||
kind = TrackingLibraryTabKind.WATCHLIST,
|
||||
)
|
||||
|
||||
internal fun simklLibraryListTab(): TraktListTab =
|
||||
TraktListTab(
|
||||
key = SIMKL_WATCHLIST_KEY,
|
||||
title = SIMKL_WATCHLIST_TITLE,
|
||||
type = TraktListType.WATCHLIST,
|
||||
)
|
||||
|
||||
internal fun libraryTabsWithLocal(traktTabs: List<TraktListTab>): List<TraktListTab> =
|
||||
listOf(localLibraryListTab()) + traktTabs
|
||||
internal fun libraryTabsWithLocal(providerTabs: List<TrackingLibraryTab>): List<TrackingLibraryTab> =
|
||||
listOf(localLibraryListTab()) + providerTabs
|
||||
|
||||
internal fun libraryMembershipWithLocal(
|
||||
inLocal: Boolean,
|
||||
traktMembership: Map<String, Boolean> = emptyMap(),
|
||||
providerMembership: Map<String, Boolean> = emptyMap(),
|
||||
): Map<String, Boolean> =
|
||||
linkedMapOf<String, Boolean>(LOCAL_LIBRARY_LIST_KEY to inLocal).apply {
|
||||
putAll(traktMembership)
|
||||
putAll(providerMembership)
|
||||
}
|
||||
|
||||
internal fun libraryMembershipWithRemovedList(
|
||||
|
|
|
|||
|
|
@ -2,9 +2,15 @@ package com.nuvio.app.features.simkl
|
|||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.nuvio.app.features.library.LibraryItem
|
||||
import com.nuvio.app.features.library.LibrarySection
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
import com.nuvio.app.features.tracking.TrackingHistoryItem
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryProvider
|
||||
import com.nuvio.app.features.tracking.TrackingLibrarySnapshot
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryTab
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryTabKind
|
||||
import com.nuvio.app.features.tracking.TrackingListStatus
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import com.nuvio.app.features.watched.WatchedItem
|
||||
import com.nuvio.app.features.watching.sync.WatchedSyncAdapter
|
||||
import com.nuvio.app.features.watchprogress.WatchProgressEntry
|
||||
|
|
@ -16,6 +22,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class SimklLibraryUiState(
|
||||
|
|
@ -96,6 +104,73 @@ object SimklLibraryRepository {
|
|||
}
|
||||
}
|
||||
|
||||
object SimklTrackingLibraryProvider : TrackingLibraryProvider {
|
||||
override val providerId: TrackingProviderId = TrackingProviderId.SIMKL
|
||||
override val changes: Flow<Unit> = SimklLibraryRepository.uiState.map { Unit }
|
||||
|
||||
override fun ensureLoaded() = SimklLibraryRepository.ensureLoaded()
|
||||
|
||||
override suspend fun refresh() = SimklLibraryRepository.refreshNow()
|
||||
|
||||
override fun snapshot(): TrackingLibrarySnapshot {
|
||||
val state = SimklLibraryRepository.uiState.value
|
||||
val items = state.items.sortedByDescending(LibraryItem::savedAtEpochMs)
|
||||
return TrackingLibrarySnapshot(
|
||||
items = items,
|
||||
sections = listOf(
|
||||
LibrarySection(
|
||||
type = SIMKL_WATCHLIST_KEY,
|
||||
displayTitle = SIMKL_WATCHLIST_TITLE,
|
||||
items = items,
|
||||
),
|
||||
),
|
||||
tabs = listOf(
|
||||
TrackingLibraryTab(
|
||||
key = SIMKL_WATCHLIST_KEY,
|
||||
title = SIMKL_WATCHLIST_TITLE,
|
||||
providerId = TrackingProviderId.SIMKL,
|
||||
kind = TrackingLibraryTabKind.WATCHLIST,
|
||||
),
|
||||
),
|
||||
hasLoaded = state.hasLoaded,
|
||||
isLoading = state.isLoading,
|
||||
errorMessage = state.errorMessage,
|
||||
)
|
||||
}
|
||||
|
||||
override fun contains(contentId: String, contentType: String?): Boolean =
|
||||
SimklLibraryRepository.isInWatchlist(contentId, contentType)
|
||||
|
||||
override fun find(contentId: String): LibraryItem? =
|
||||
SimklLibraryRepository.uiState.value.items.firstOrNull { item -> item.id == contentId }
|
||||
|
||||
override suspend fun membership(item: LibraryItem): Map<String, Boolean> =
|
||||
mapOf(SIMKL_WATCHLIST_KEY to SimklLibraryRepository.isInWatchlist(item.id, item.type))
|
||||
|
||||
override suspend fun applyMembership(
|
||||
profileId: Int,
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
) {
|
||||
val desired = desiredMembership[SIMKL_WATCHLIST_KEY] == true
|
||||
if (desired != SimklLibraryRepository.isInWatchlist(item.id, item.type)) {
|
||||
SimklLibraryRepository.setWatchlistMembership(
|
||||
profileId = profileId,
|
||||
item = item,
|
||||
isMember = desired,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun toggleDefaultMembership(profileId: Int, item: LibraryItem) {
|
||||
SimklLibraryRepository.setWatchlistMembership(
|
||||
profileId = profileId,
|
||||
item = item,
|
||||
isMember = !SimklLibraryRepository.isInWatchlist(item.id, item.type),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object SimklWatchedSyncAdapter : WatchedSyncAdapter {
|
||||
override suspend fun pull(profileId: Int, pageSize: Int): List<WatchedItem> {
|
||||
if (profileId != ProfileRepository.activeProfileId) return emptyList()
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ object TrackingProviderRegistry {
|
|||
private val listWriters = mutableMapOf<TrackingProviderId, TrackingListWriter>()
|
||||
private val historyWriters = mutableMapOf<TrackingProviderId, TrackingHistoryWriter>()
|
||||
private val scrobblers = mutableMapOf<TrackingProviderId, TrackingScrobbler>()
|
||||
private val libraryProviders = mutableMapOf<TrackingProviderId, TrackingLibraryProvider>()
|
||||
|
||||
private val _connectedProviderIds = MutableStateFlow<Set<TrackingProviderId>>(emptySet())
|
||||
val connectedProviderIds: StateFlow<Set<TrackingProviderId>> = _connectedProviderIds.asStateFlow()
|
||||
|
|
@ -113,6 +114,10 @@ object TrackingProviderRegistry {
|
|||
scrobblers[scrobbler.providerId] = scrobbler
|
||||
}
|
||||
|
||||
fun registerLibraryProvider(provider: TrackingLibraryProvider) = synchronized(lock) {
|
||||
libraryProviders[provider.providerId] = provider
|
||||
}
|
||||
|
||||
fun authProvider(id: TrackingProviderId): TrackingAuthProvider? = synchronized(lock) {
|
||||
authProviders[id]
|
||||
}
|
||||
|
|
@ -144,6 +149,16 @@ object TrackingProviderRegistry {
|
|||
scrobblers[id]
|
||||
}
|
||||
|
||||
fun libraryProvider(id: TrackingProviderId): TrackingLibraryProvider? = synchronized(lock) {
|
||||
libraryProviders[id]
|
||||
}
|
||||
|
||||
fun libraryProviders(): List<TrackingLibraryProvider> = synchronized(lock) {
|
||||
libraryProviders.entries
|
||||
.sortedBy { (id, _) -> id.ordinal }
|
||||
.map { (_, provider) -> provider }
|
||||
}
|
||||
|
||||
fun connectedListWriters(): List<TrackingListWriter> =
|
||||
connectedPorts(listWriters, TrackingCapability.LIBRARY_WRITE)
|
||||
|
||||
|
|
@ -153,6 +168,9 @@ object TrackingProviderRegistry {
|
|||
fun connectedScrobblers(): List<TrackingScrobbler> =
|
||||
connectedPorts(scrobblers, TrackingCapability.SCROBBLE)
|
||||
|
||||
fun connectedLibraryProviders(): List<TrackingLibraryProvider> =
|
||||
connectedPorts(libraryProviders, TrackingCapability.LIBRARY_READ)
|
||||
|
||||
fun handleAuthCallback(url: String): Boolean =
|
||||
providersWith(TrackingCapability.AUTHENTICATION)
|
||||
.any { provider -> provider.handleAuthCallback(url) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package com.nuvio.app.features.tracking
|
||||
|
||||
import com.nuvio.app.features.library.LibraryItem
|
||||
import com.nuvio.app.features.library.LibrarySection
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
enum class TrackingLibraryTabKind {
|
||||
WATCHLIST,
|
||||
PERSONAL,
|
||||
}
|
||||
|
||||
data class TrackingLibraryTab(
|
||||
val key: String,
|
||||
val title: String,
|
||||
val providerId: TrackingProviderId?,
|
||||
val kind: TrackingLibraryTabKind,
|
||||
)
|
||||
|
||||
data class TrackingLibrarySnapshot(
|
||||
val items: List<LibraryItem> = emptyList(),
|
||||
val sections: List<LibrarySection> = emptyList(),
|
||||
val tabs: List<TrackingLibraryTab> = emptyList(),
|
||||
val hasLoaded: Boolean = false,
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Provider-owned projection of remote library state into application models.
|
||||
*
|
||||
* Application repositories consume this port through [TrackingProviderRegistry]; provider DTOs,
|
||||
* list semantics, cache policy, and mutation details stay inside the provider package.
|
||||
*/
|
||||
interface TrackingLibraryProvider {
|
||||
val providerId: TrackingProviderId
|
||||
val changes: Flow<Unit>
|
||||
|
||||
fun ensureLoaded()
|
||||
fun prepare() = Unit
|
||||
fun onProfileChanged() = Unit
|
||||
fun clearLocalState() = Unit
|
||||
suspend fun refresh()
|
||||
fun snapshot(): TrackingLibrarySnapshot
|
||||
fun contains(contentId: String, contentType: String? = null): Boolean
|
||||
fun find(contentId: String): LibraryItem?
|
||||
suspend fun membership(item: LibraryItem): Map<String, Boolean>
|
||||
suspend fun applyMembership(
|
||||
profileId: Int,
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
)
|
||||
suspend fun toggleDefaultMembership(profileId: Int, item: LibraryItem)
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.nuvio.app.features.trakt
|
||||
|
||||
import com.nuvio.app.features.library.LibraryItem
|
||||
import com.nuvio.app.features.library.LibrarySection
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryProvider
|
||||
import com.nuvio.app.features.tracking.TrackingLibrarySnapshot
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryTab
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryTabKind
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
object TraktTrackingLibraryProvider : TrackingLibraryProvider {
|
||||
override val providerId: TrackingProviderId = TrackingProviderId.TRAKT
|
||||
override val changes: Flow<Unit> = TraktLibraryRepository.uiState.map { Unit }
|
||||
|
||||
override fun ensureLoaded() = TraktLibraryRepository.ensureLoaded()
|
||||
|
||||
override fun prepare() = TraktLibraryRepository.preloadListTabsAsync()
|
||||
|
||||
override fun onProfileChanged() = TraktLibraryRepository.onProfileChanged()
|
||||
|
||||
override fun clearLocalState() = TraktLibraryRepository.clearLocalState()
|
||||
|
||||
override suspend fun refresh() = TraktLibraryRepository.refreshNow()
|
||||
|
||||
override fun snapshot(): TrackingLibrarySnapshot {
|
||||
val state = TraktLibraryRepository.uiState.value
|
||||
return TrackingLibrarySnapshot(
|
||||
items = state.allItems,
|
||||
sections = state.listTabs.mapNotNull { tab ->
|
||||
state.entriesByList[tab.key]
|
||||
.orEmpty()
|
||||
.takeIf(List<LibraryItem>::isNotEmpty)
|
||||
?.let { items ->
|
||||
LibrarySection(
|
||||
type = tab.key,
|
||||
displayTitle = tab.title,
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
},
|
||||
tabs = state.listTabs.map(TraktListTab::toTrackingLibraryTab),
|
||||
hasLoaded = state.hasLoaded,
|
||||
isLoading = state.isLoading,
|
||||
errorMessage = state.errorMessage,
|
||||
)
|
||||
}
|
||||
|
||||
override fun contains(contentId: String, contentType: String?): Boolean {
|
||||
if (contentType != null) return TraktLibraryRepository.isInAnyList(contentId, contentType)
|
||||
val item = find(contentId) ?: return false
|
||||
return TraktLibraryRepository.isInAnyList(item.id, item.type)
|
||||
}
|
||||
|
||||
override fun find(contentId: String): LibraryItem? =
|
||||
TraktLibraryRepository.uiState.value.allItems.firstOrNull { item -> item.id == contentId }
|
||||
|
||||
override suspend fun membership(item: LibraryItem): Map<String, Boolean> =
|
||||
TraktLibraryRepository.getMembershipSnapshot(item).listMembership
|
||||
|
||||
override suspend fun applyMembership(
|
||||
profileId: Int,
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
) {
|
||||
TraktLibraryRepository.applyMembershipChanges(
|
||||
item = item,
|
||||
changes = TraktMembershipChanges(desiredMembership = desiredMembership),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun toggleDefaultMembership(profileId: Int, item: LibraryItem) =
|
||||
TraktLibraryRepository.toggleWatchlist(item)
|
||||
}
|
||||
|
||||
private fun TraktListTab.toTrackingLibraryTab(): TrackingLibraryTab =
|
||||
TrackingLibraryTab(
|
||||
key = key,
|
||||
title = title,
|
||||
providerId = TrackingProviderId.TRAKT,
|
||||
kind = when (type) {
|
||||
TraktListType.WATCHLIST -> TrackingLibraryTabKind.WATCHLIST
|
||||
TraktListType.PERSONAL -> TrackingLibraryTabKind.PERSONAL
|
||||
},
|
||||
)
|
||||
|
|
@ -3,8 +3,9 @@ package com.nuvio.app.features.library
|
|||
import com.nuvio.app.features.details.MetaDetails
|
||||
import com.nuvio.app.features.home.PosterShape
|
||||
import com.nuvio.app.features.home.MetaPreview
|
||||
import com.nuvio.app.features.trakt.TraktListTab
|
||||
import com.nuvio.app.features.trakt.TraktListType
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryTab
|
||||
import com.nuvio.app.features.tracking.TrackingLibraryTabKind
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
|
|
@ -71,10 +72,11 @@ class LibraryRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun `library tabs include local Nuvio library before Trakt tabs`() {
|
||||
val traktTab = TraktListTab(
|
||||
val traktTab = TrackingLibraryTab(
|
||||
key = "trakt:watchlist",
|
||||
title = "Watchlist",
|
||||
type = TraktListType.WATCHLIST,
|
||||
providerId = TrackingProviderId.TRAKT,
|
||||
kind = TrackingLibraryTabKind.WATCHLIST,
|
||||
)
|
||||
|
||||
val tabs = libraryTabsWithLocal(listOf(traktTab))
|
||||
|
|
@ -87,7 +89,7 @@ class LibraryRepositoryTest {
|
|||
fun `library membership always includes local state before Trakt membership`() {
|
||||
val membership = libraryMembershipWithLocal(
|
||||
inLocal = true,
|
||||
traktMembership = mapOf("trakt:watchlist" to false),
|
||||
providerMembership = mapOf("trakt:watchlist" to false),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
|
|
|||
Loading…
Reference in a new issue