mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
feat: implement Trakt library storage and initialization
This commit is contained in:
parent
a37c962b98
commit
38ef25e83c
8 changed files with 204 additions and 12 deletions
|
|
@ -31,6 +31,7 @@ import com.nuvio.app.features.search.SearchHistoryStorage
|
||||||
import com.nuvio.app.features.settings.ThemeSettingsStorage
|
import com.nuvio.app.features.settings.ThemeSettingsStorage
|
||||||
import com.nuvio.app.features.trakt.TraktAuthStorage
|
import com.nuvio.app.features.trakt.TraktAuthStorage
|
||||||
import com.nuvio.app.features.trakt.TraktCommentsStorage
|
import com.nuvio.app.features.trakt.TraktCommentsStorage
|
||||||
|
import com.nuvio.app.features.trakt.TraktLibraryStorage
|
||||||
import com.nuvio.app.features.tmdb.TmdbSettingsStorage
|
import com.nuvio.app.features.tmdb.TmdbSettingsStorage
|
||||||
import com.nuvio.app.core.ui.PosterCardStyleStorage
|
import com.nuvio.app.core.ui.PosterCardStyleStorage
|
||||||
import com.nuvio.app.features.watched.WatchedStorage
|
import com.nuvio.app.features.watched.WatchedStorage
|
||||||
|
|
@ -66,6 +67,7 @@ class MainActivity : ComponentActivity() {
|
||||||
MdbListSettingsStorage.initialize(applicationContext)
|
MdbListSettingsStorage.initialize(applicationContext)
|
||||||
TraktAuthStorage.initialize(applicationContext)
|
TraktAuthStorage.initialize(applicationContext)
|
||||||
TraktCommentsStorage.initialize(applicationContext)
|
TraktCommentsStorage.initialize(applicationContext)
|
||||||
|
TraktLibraryStorage.initialize(applicationContext)
|
||||||
ContinueWatchingPreferencesStorage.initialize(applicationContext)
|
ContinueWatchingPreferencesStorage.initialize(applicationContext)
|
||||||
ResumePromptStorage.initialize(applicationContext)
|
ResumePromptStorage.initialize(applicationContext)
|
||||||
ContinueWatchingEnrichmentStorage.initialize(applicationContext)
|
ContinueWatchingEnrichmentStorage.initialize(applicationContext)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ internal actual object PlatformLocalAccountDataCleaner {
|
||||||
"nuvio_poster_card_style",
|
"nuvio_poster_card_style",
|
||||||
"nuvio_mdblist_settings",
|
"nuvio_mdblist_settings",
|
||||||
"nuvio_trakt_auth",
|
"nuvio_trakt_auth",
|
||||||
|
"nuvio_trakt_library",
|
||||||
"nuvio_watched",
|
"nuvio_watched",
|
||||||
"nuvio_stream_link_cache",
|
"nuvio_stream_link_cache",
|
||||||
"nuvio_continue_watching_preferences",
|
"nuvio_continue_watching_preferences",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.nuvio.app.features.trakt
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
|
||||||
|
internal actual object TraktLibraryStorage {
|
||||||
|
private const val preferencesName = "nuvio_trakt_library"
|
||||||
|
private const val payloadKey = "trakt_library_payload"
|
||||||
|
|
||||||
|
private var preferences: SharedPreferences? = null
|
||||||
|
|
||||||
|
fun initialize(context: Context) {
|
||||||
|
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
preferences?.getString(ProfileScopedKey.of(payloadKey), null)
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
preferences
|
||||||
|
?.edit()
|
||||||
|
?.putString(ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
?.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,8 +8,10 @@ import com.nuvio.app.features.details.MetaDetailsRepository
|
||||||
import com.nuvio.app.features.library.LibraryItem
|
import com.nuvio.app.features.library.LibraryItem
|
||||||
import com.nuvio.app.features.tmdb.TmdbService
|
import com.nuvio.app.features.tmdb.TmdbService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
|
@ -22,6 +24,7 @@ import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import kotlinx.coroutines.selects.select
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
|
|
@ -52,7 +55,10 @@ data class TraktLibraryUiState(
|
||||||
|
|
||||||
object TraktLibraryRepository {
|
object TraktLibraryRepository {
|
||||||
private val log = Logger.withTag("TraktLibrary")
|
private val log = Logger.withTag("TraktLibrary")
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
encodeDefaults = true
|
||||||
|
}
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(TraktLibraryUiState())
|
private val _uiState = MutableStateFlow(TraktLibraryUiState())
|
||||||
|
|
@ -60,12 +66,14 @@ object TraktLibraryRepository {
|
||||||
|
|
||||||
private var hasLoaded = false
|
private var hasLoaded = false
|
||||||
private val refreshMutex = Mutex()
|
private val refreshMutex = Mutex()
|
||||||
|
private var hydrationJob: Job? = null
|
||||||
private var lastRefreshAtMs: Long = 0L
|
private var lastRefreshAtMs: Long = 0L
|
||||||
private var lastListTabsRefreshAtMs: Long = 0L
|
private var lastListTabsRefreshAtMs: Long = 0L
|
||||||
|
|
||||||
fun ensureLoaded() {
|
fun ensureLoaded() {
|
||||||
if (hasLoaded) return
|
if (hasLoaded) return
|
||||||
hasLoaded = true
|
hasLoaded = true
|
||||||
|
loadSnapshotFromDisk()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun preloadListTabsAsync() {
|
fun preloadListTabsAsync() {
|
||||||
|
|
@ -81,6 +89,8 @@ object TraktLibraryRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onProfileChanged() {
|
fun onProfileChanged() {
|
||||||
|
hydrationJob?.cancel()
|
||||||
|
hydrationJob = null
|
||||||
hasLoaded = false
|
hasLoaded = false
|
||||||
lastRefreshAtMs = 0L
|
lastRefreshAtMs = 0L
|
||||||
lastListTabsRefreshAtMs = 0L
|
lastListTabsRefreshAtMs = 0L
|
||||||
|
|
@ -89,10 +99,13 @@ object TraktLibraryRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearLocalState() {
|
fun clearLocalState() {
|
||||||
|
hydrationJob?.cancel()
|
||||||
|
hydrationJob = null
|
||||||
hasLoaded = false
|
hasLoaded = false
|
||||||
lastRefreshAtMs = 0L
|
lastRefreshAtMs = 0L
|
||||||
lastListTabsRefreshAtMs = 0L
|
lastListTabsRefreshAtMs = 0L
|
||||||
_uiState.value = TraktLibraryUiState()
|
_uiState.value = TraktLibraryUiState()
|
||||||
|
TraktLibraryStorage.savePayload("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun currentListTabs(): List<TraktListTab> = _uiState.value.listTabs
|
fun currentListTabs(): List<TraktListTab> = _uiState.value.listTabs
|
||||||
|
|
@ -152,7 +165,14 @@ object TraktLibraryRepository {
|
||||||
_uiState.value = current.copy(isLoading = true, errorMessage = null)
|
_uiState.value = current.copy(isLoading = true, errorMessage = null)
|
||||||
|
|
||||||
val result = runCatching {
|
val result = runCatching {
|
||||||
fetchSnapshot(headers)
|
fetchSnapshot(headers) { partialState ->
|
||||||
|
_uiState.value = partialState.copy(
|
||||||
|
isLoading = true,
|
||||||
|
hasLoaded = true,
|
||||||
|
errorMessage = null,
|
||||||
|
)
|
||||||
|
hydrateMissingMetadataAsync(_uiState.value)
|
||||||
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
if (error is CancellationException) throw error
|
if (error is CancellationException) throw error
|
||||||
log.w { "Failed to refresh Trakt library: ${error.message}" }
|
log.w { "Failed to refresh Trakt library: ${error.message}" }
|
||||||
|
|
@ -172,6 +192,8 @@ object TraktLibraryRepository {
|
||||||
hasLoaded = true,
|
hasLoaded = true,
|
||||||
errorMessage = null,
|
errorMessage = null,
|
||||||
)
|
)
|
||||||
|
persistSnapshot(_uiState.value)
|
||||||
|
hydrateMissingMetadataAsync(_uiState.value)
|
||||||
lastRefreshAtMs = now
|
lastRefreshAtMs = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -322,11 +344,15 @@ object TraktLibraryRepository {
|
||||||
allItems = allItems,
|
allItems = allItems,
|
||||||
membershipByContent = membershipByContent.mapValues { it.value.toSet() },
|
membershipByContent = membershipByContent.mapValues { it.value.toSet() },
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
hasLoaded = true,
|
||||||
errorMessage = null,
|
errorMessage = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchSnapshot(headers: Map<String, String>): TraktLibraryUiState = withContext(Dispatchers.Default) {
|
private suspend fun fetchSnapshot(
|
||||||
|
headers: Map<String, String>,
|
||||||
|
onPartialState: ((TraktLibraryUiState) -> Unit)? = null,
|
||||||
|
): TraktLibraryUiState = withContext(Dispatchers.Default) {
|
||||||
val now = TraktPlatformClock.nowEpochMs()
|
val now = TraktPlatformClock.nowEpochMs()
|
||||||
val cachedTabs = _uiState.value.listTabs
|
val cachedTabs = _uiState.value.listTabs
|
||||||
val allTabs = if (
|
val allTabs = if (
|
||||||
|
|
@ -340,12 +366,23 @@ object TraktLibraryRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val entriesByList = fetchEntriesByList(headers, allTabs)
|
val entriesByList = fetchEntriesByList(
|
||||||
|
headers = headers,
|
||||||
val hydratedEntriesByList = hydrateEntriesFromAddonMeta(entriesByList)
|
allTabs = allTabs,
|
||||||
|
onProgress = onPartialState?.let { emitPartial ->
|
||||||
|
{ partialEntriesByList ->
|
||||||
|
emitPartial(
|
||||||
|
rebuildUiState(
|
||||||
|
listTabs = allTabs,
|
||||||
|
entriesByList = partialEntriesByList,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
val membershipByContent = mutableMapOf<String, MutableSet<String>>()
|
val membershipByContent = mutableMapOf<String, MutableSet<String>>()
|
||||||
hydratedEntriesByList.forEach { (listKey, entries) ->
|
entriesByList.forEach { (listKey, entries) ->
|
||||||
entries.forEach { entry ->
|
entries.forEach { entry ->
|
||||||
membershipByContent
|
membershipByContent
|
||||||
.getOrPut(contentKey(entry.id, entry.type)) { mutableSetOf() }
|
.getOrPut(contentKey(entry.id, entry.type)) { mutableSetOf() }
|
||||||
|
|
@ -353,19 +390,99 @@ object TraktLibraryRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val allItems = hydratedEntriesByList.values
|
val allItems = entriesByList.values
|
||||||
.flatten()
|
.flatten()
|
||||||
.distinctBy { contentKey(it.id, it.type) }
|
.distinctBy { contentKey(it.id, it.type) }
|
||||||
.sortedByDescending { it.savedAtEpochMs }
|
.sortedByDescending { it.savedAtEpochMs }
|
||||||
|
|
||||||
TraktLibraryUiState(
|
TraktLibraryUiState(
|
||||||
listTabs = allTabs,
|
listTabs = allTabs,
|
||||||
entriesByList = hydratedEntriesByList,
|
entriesByList = entriesByList,
|
||||||
allItems = allItems,
|
allItems = allItems,
|
||||||
membershipByContent = membershipByContent.mapValues { it.value.toSet() },
|
membershipByContent = membershipByContent.mapValues { it.value.toSet() },
|
||||||
|
hasLoaded = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadSnapshotFromDisk() {
|
||||||
|
val payload = TraktLibraryStorage.loadPayload().orEmpty().trim()
|
||||||
|
if (payload.isBlank()) return
|
||||||
|
|
||||||
|
val cached = runCatching {
|
||||||
|
json.decodeFromString<StoredTraktLibraryPayload>(payload)
|
||||||
|
}.onFailure {
|
||||||
|
log.w { "Failed to parse cached Trakt library payload: ${it.message}" }
|
||||||
|
}.getOrNull() ?: return
|
||||||
|
|
||||||
|
val state = rebuildUiState(
|
||||||
|
listTabs = cached.listTabs,
|
||||||
|
entriesByList = cached.entriesByList,
|
||||||
|
)
|
||||||
|
_uiState.value = state.copy(isLoading = false, errorMessage = null, hasLoaded = true)
|
||||||
|
hydrateMissingMetadataAsync(_uiState.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun persistSnapshot(state: TraktLibraryUiState) {
|
||||||
|
val payload = StoredTraktLibraryPayload(
|
||||||
|
listTabs = state.listTabs,
|
||||||
|
entriesByList = state.entriesByList,
|
||||||
|
)
|
||||||
|
TraktLibraryStorage.savePayload(json.encodeToString(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hydrateMissingMetadataAsync(state: TraktLibraryUiState) {
|
||||||
|
if (state.entriesByList.isEmpty()) return
|
||||||
|
if (state.allItems.none(::shouldHydrateTraktLibraryItem)) return
|
||||||
|
|
||||||
|
hydrationJob?.cancel()
|
||||||
|
hydrationJob = scope.launch {
|
||||||
|
val hydratedEntriesByList = runCatching {
|
||||||
|
hydrateEntriesFromAddonMeta(state.entriesByList)
|
||||||
|
}.onFailure { error ->
|
||||||
|
if (error is CancellationException) throw error
|
||||||
|
log.w { "Background Trakt metadata hydration failed: ${error.message}" }
|
||||||
|
}.getOrNull() ?: return@launch
|
||||||
|
|
||||||
|
refreshMutex.withLock {
|
||||||
|
val current = _uiState.value
|
||||||
|
if (current.entriesByList.isEmpty()) return@withLock
|
||||||
|
|
||||||
|
val mergedEntriesByList = mergeHydratedEntries(
|
||||||
|
currentEntriesByList = current.entriesByList,
|
||||||
|
hydratedEntriesByList = hydratedEntriesByList,
|
||||||
|
)
|
||||||
|
if (mergedEntriesByList == current.entriesByList) return@withLock
|
||||||
|
|
||||||
|
val rebuilt = rebuildUiState(
|
||||||
|
listTabs = current.listTabs,
|
||||||
|
entriesByList = mergedEntriesByList,
|
||||||
|
).copy(
|
||||||
|
isLoading = current.isLoading,
|
||||||
|
hasLoaded = current.hasLoaded,
|
||||||
|
errorMessage = current.errorMessage,
|
||||||
|
)
|
||||||
|
|
||||||
|
_uiState.value = rebuilt
|
||||||
|
persistSnapshot(rebuilt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mergeHydratedEntries(
|
||||||
|
currentEntriesByList: Map<String, List<LibraryItem>>,
|
||||||
|
hydratedEntriesByList: Map<String, List<LibraryItem>>,
|
||||||
|
): Map<String, List<LibraryItem>> {
|
||||||
|
val hydratedByContentKey = hydratedEntriesByList.values
|
||||||
|
.flatten()
|
||||||
|
.associateBy { contentKey(it.id, it.type) }
|
||||||
|
|
||||||
|
return currentEntriesByList.mapValues { (_, entries) ->
|
||||||
|
entries.map { entry ->
|
||||||
|
hydratedByContentKey[contentKey(entry.id, entry.type)] ?: entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun fetchListTabs(headers: Map<String, String>): List<TraktListTab> {
|
private suspend fun fetchListTabs(headers: Map<String, String>): List<TraktListTab> {
|
||||||
val watchlistTabs = listOf(
|
val watchlistTabs = listOf(
|
||||||
TraktListTab(
|
TraktListTab(
|
||||||
|
|
@ -380,8 +497,12 @@ object TraktLibraryRepository {
|
||||||
private suspend fun fetchEntriesByList(
|
private suspend fun fetchEntriesByList(
|
||||||
headers: Map<String, String>,
|
headers: Map<String, String>,
|
||||||
allTabs: List<TraktListTab>,
|
allTabs: List<TraktListTab>,
|
||||||
|
onProgress: ((Map<String, List<LibraryItem>>) -> Unit)? = null,
|
||||||
): Map<String, List<LibraryItem>> = coroutineScope {
|
): Map<String, List<LibraryItem>> = coroutineScope {
|
||||||
val entriesByList = linkedMapOf<String, List<LibraryItem>>()
|
val entriesByList = linkedMapOf<String, List<LibraryItem>>()
|
||||||
|
allTabs.forEach { tab ->
|
||||||
|
entriesByList[tab.key] = emptyList()
|
||||||
|
}
|
||||||
val listSemaphore = Semaphore(LIST_FETCH_CONCURRENCY)
|
val listSemaphore = Semaphore(LIST_FETCH_CONCURRENCY)
|
||||||
val personalTabs = allTabs.filter { it.type == TraktListType.PERSONAL }
|
val personalTabs = allTabs.filter { it.type == TraktListType.PERSONAL }
|
||||||
|
|
||||||
|
|
@ -404,10 +525,21 @@ object TraktLibraryRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
entriesByList[WATCHLIST_KEY] = watchlistDeferred.await()
|
entriesByList[WATCHLIST_KEY] = watchlistDeferred.await()
|
||||||
personalTabs.forEach { tab ->
|
onProgress?.invoke(entriesByList.toMap())
|
||||||
entriesByList[tab.key] = personalEntries.getValue(tab.key).await()
|
|
||||||
|
val pendingEntries = personalEntries.toMutableMap()
|
||||||
|
while (pendingEntries.isNotEmpty()) {
|
||||||
|
val (listKey, listItems) = select<Pair<String, List<LibraryItem>>> {
|
||||||
|
pendingEntries.forEach { (key, deferred) ->
|
||||||
|
deferred.onAwait { key to it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entriesByList[listKey] = listItems
|
||||||
|
pendingEntries.remove(listKey)
|
||||||
|
onProgress?.invoke(entriesByList.toMap())
|
||||||
}
|
}
|
||||||
entriesByList
|
|
||||||
|
entriesByList.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun hydrateEntriesFromAddonMeta(
|
private suspend fun hydrateEntriesFromAddonMeta(
|
||||||
|
|
@ -653,6 +785,7 @@ object TraktLibraryRepository {
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
val poster = media.images?.poster.firstNonBlankImageUrl()
|
val poster = media.images?.poster.firstNonBlankImageUrl()
|
||||||
|
?: media.images?.fanart.firstNonBlankImageUrl()
|
||||||
val banner = media.images?.banner.firstNonBlankImageUrl()
|
val banner = media.images?.banner.firstNonBlankImageUrl()
|
||||||
val logo = media.images?.logo.firstNonBlankImageUrl()
|
val logo = media.images?.logo.firstNonBlankImageUrl()
|
||||||
|
|
||||||
|
|
@ -725,6 +858,12 @@ object TraktLibraryRepository {
|
||||||
private val imdbRegex = Regex("tt\\d+")
|
private val imdbRegex = Regex("tt\\d+")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class StoredTraktLibraryPayload(
|
||||||
|
val listTabs: List<TraktListTab> = emptyList(),
|
||||||
|
val entriesByList: Map<String, List<LibraryItem>> = emptyMap(),
|
||||||
|
)
|
||||||
|
|
||||||
internal fun shouldHydrateTraktLibraryItem(item: LibraryItem): Boolean {
|
internal fun shouldHydrateTraktLibraryItem(item: LibraryItem): Boolean {
|
||||||
val missingDisplayName = item.name.isBlank() || item.name == item.id
|
val missingDisplayName = item.name.isBlank() || item.name == item.id
|
||||||
return missingDisplayName || item.poster.isNullOrBlank() || item.releaseInfo.isNullOrBlank()
|
return missingDisplayName || item.poster.isNullOrBlank() || item.releaseInfo.isNullOrBlank()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.nuvio.app.features.trakt
|
||||||
|
|
||||||
|
internal expect object TraktLibraryStorage {
|
||||||
|
fun loadPayload(): String?
|
||||||
|
fun savePayload(payload: String)
|
||||||
|
}
|
||||||
|
|
@ -40,11 +40,13 @@ enum class TraktBrandAsset {
|
||||||
Wordmark,
|
Wordmark,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
enum class TraktListType {
|
enum class TraktListType {
|
||||||
WATCHLIST,
|
WATCHLIST,
|
||||||
PERSONAL,
|
PERSONAL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class TraktListTab(
|
data class TraktListTab(
|
||||||
val key: String,
|
val key: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ internal actual object PlatformLocalAccountDataCleaner {
|
||||||
"mdblist_use_letterboxd",
|
"mdblist_use_letterboxd",
|
||||||
"mdblist_use_audience",
|
"mdblist_use_audience",
|
||||||
"trakt_auth_payload",
|
"trakt_auth_payload",
|
||||||
|
"trakt_library_payload",
|
||||||
)
|
)
|
||||||
|
|
||||||
actual fun wipe() {
|
actual fun wipe() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.nuvio.app.features.trakt
|
||||||
|
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import platform.Foundation.NSUserDefaults
|
||||||
|
|
||||||
|
internal actual object TraktLibraryStorage {
|
||||||
|
private const val payloadKey = "trakt_library_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
NSUserDefaults.standardUserDefaults.setObject(payload, forKey = ProfileScopedKey.of(payloadKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue