mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-29 15:49:30 +00:00
fix(simkl): confirm destructive library removals
This commit is contained in:
parent
7fb8206107
commit
4df54ab89f
13 changed files with 600 additions and 94 deletions
|
|
@ -13,6 +13,7 @@
|
|||
<string name="action_play">Play</string>
|
||||
<string name="action_previous">Previous</string>
|
||||
<string name="action_remove">Remove</string>
|
||||
<string name="action_remove_anyway">Remove anyway</string>
|
||||
<string name="action_reorder">Reorder</string>
|
||||
<string name="action_reset">Reset to Default</string>
|
||||
<string name="action_resume">Resume</string>
|
||||
|
|
@ -1467,6 +1468,11 @@
|
|||
<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="tracking_list_status_rewritten">%1$s placed this title in %2$s instead of %3$s</string>
|
||||
<string name="tracking_remove_confirmation_title">Remove from %1$s?</string>
|
||||
<string name="tracking_remove_confirmation_message">Removing “%1$s” from %2$s will also clear its %3$s there. This can’t be undone.</string>
|
||||
<string name="tracking_removal_impact_history">watched history</string>
|
||||
<string name="tracking_removal_impact_rating">rating</string>
|
||||
<string name="tracking_removal_impact_history_and_rating">watched history and rating</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>
|
||||
|
|
|
|||
|
|
@ -107,6 +107,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.PosterZoomOverlayExitAnimation
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
import com.nuvio.app.core.ui.NuvioStatusModal
|
||||
|
|
@ -162,6 +163,9 @@ import com.nuvio.app.features.library.LibraryRepository
|
|||
import com.nuvio.app.features.library.LibrarySection
|
||||
import com.nuvio.app.features.library.LibrarySortOption
|
||||
import com.nuvio.app.features.library.LibrarySourceMode
|
||||
import com.nuvio.app.features.library.PendingTrackingMembershipRemoval
|
||||
import com.nuvio.app.features.library.TrackingMembershipRemovalConfirmationHost
|
||||
import com.nuvio.app.features.library.executeTrackingMembershipOperation
|
||||
import com.nuvio.app.features.library.showTrackingMembershipRewriteFeedback
|
||||
import com.nuvio.app.features.library.LibraryScreen
|
||||
import com.nuvio.app.features.library.toLibraryItem
|
||||
|
|
@ -226,6 +230,8 @@ 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.tracking.TrackingLibraryTab
|
||||
import com.nuvio.app.features.tracking.TrackingMembershipApplyResult
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import com.nuvio.app.features.tracking.toggleTrackingLibraryMembership
|
||||
import com.nuvio.app.features.updater.AppUpdaterHost
|
||||
import com.nuvio.app.features.updater.AppUpdaterPlatform
|
||||
|
|
@ -820,6 +826,8 @@ private fun MainAppContent(
|
|||
var pickerMembership by remember { mutableStateOf<Map<String, Boolean>>(emptyMap()) }
|
||||
var pickerPending by remember { mutableStateOf(false) }
|
||||
var pickerError by remember { mutableStateOf<String?>(null) }
|
||||
var pendingTrackingRemoval by remember { mutableStateOf<PendingTrackingMembershipRemoval?>(null) }
|
||||
val trackingListsUpdateFailedMessage = stringResource(Res.string.tracking_lists_update_failed)
|
||||
val addonsUiState by remember {
|
||||
AddonRepository.initialize()
|
||||
AddonRepository.uiState
|
||||
|
|
@ -3300,8 +3308,6 @@ private fun MainAppContent(
|
|||
watchedKeys = watchedUiState.watchedKeys,
|
||||
item = preview,
|
||||
)
|
||||
// Remote-library items long-pressed outside the library open the list picker
|
||||
// instead of removing, so only true removals disintegrate.
|
||||
val removesFromLibrary = isSaved &&
|
||||
(posterActionTarget.libraryItem != null || !isRemoteLibrarySource)
|
||||
NuvioPosterZoomActionOverlay(
|
||||
|
|
@ -3324,35 +3330,66 @@ private fun MainAppContent(
|
|||
stringResource(Res.string.hero_add_to_library)
|
||||
},
|
||||
isDestructive = removesFromLibrary,
|
||||
exitAnimation = if (removesFromLibrary && !isRemoteLibrarySource) {
|
||||
PosterZoomOverlayExitAnimation.DISINTEGRATE
|
||||
} else {
|
||||
PosterZoomOverlayExitAnimation.COLLAPSE
|
||||
},
|
||||
onSelected = {
|
||||
val libraryItem = posterActionTarget.libraryItem
|
||||
?: preview.toLibraryItem(savedAtEpochMs = 0L)
|
||||
if (posterActionTarget.libraryItem != null) {
|
||||
if (isRemoteLibrarySource) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
val listKey = posterActionTarget.libraryListKey
|
||||
val listKey = posterActionTarget.libraryListKey
|
||||
val removeMembership: suspend (Set<TrackingProviderId>) ->
|
||||
TrackingMembershipApplyResult = { confirmedProviders ->
|
||||
if (listKey.isNullOrBlank()) {
|
||||
val currentMembership = LibraryRepository.getMembershipSnapshot(libraryItem)
|
||||
LibraryRepository.applyMembershipChanges(
|
||||
item = libraryItem,
|
||||
desiredMembership = currentMembership.mapValues { false },
|
||||
confirmedRemovalProviders = confirmedProviders,
|
||||
)
|
||||
} else {
|
||||
LibraryRepository.removeFromList(libraryItem, listKey)
|
||||
LibraryRepository.removeFromList(
|
||||
item = libraryItem,
|
||||
listKey = listKey,
|
||||
confirmedRemovalProviders = confirmedProviders,
|
||||
)
|
||||
}
|
||||
}.onFailure { error ->
|
||||
NuvioToastController.show(
|
||||
error.message ?: getString(Res.string.tracking_lists_update_failed),
|
||||
)
|
||||
}
|
||||
executeTrackingMembershipOperation(
|
||||
operation = { removeMembership(emptySet()) },
|
||||
onSuccess = { result ->
|
||||
if (result.requiresRemovalConfirmation) {
|
||||
pendingTrackingRemoval = PendingTrackingMembershipRemoval(
|
||||
itemTitle = libraryItem.name,
|
||||
confirmations = result.requiredRemovalConfirmations,
|
||||
retry = removeMembership,
|
||||
onApplied = {},
|
||||
onFailure = { error ->
|
||||
NuvioToastController.show(
|
||||
error.message
|
||||
?: trackingListsUpdateFailedMessage,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
NuvioToastController.show(
|
||||
error.message ?: trackingListsUpdateFailedMessage,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LibraryRepository.remove(libraryItem.id)
|
||||
}
|
||||
} else {
|
||||
if (!isRemoteLibrarySource) {
|
||||
LibraryRepository.toggleSaved(libraryItem)
|
||||
LibraryRepository.toggleLocalSaved(libraryItem)
|
||||
} else {
|
||||
pickerItem = libraryItem
|
||||
pickerTitle = preview.name
|
||||
|
|
@ -3519,24 +3556,52 @@ private fun MainAppContent(
|
|||
coroutineScope.launch {
|
||||
pickerPending = true
|
||||
pickerError = null
|
||||
runCatching {
|
||||
val desiredMembership = pickerMembership.toMap()
|
||||
val applyMembership: suspend (Set<TrackingProviderId>) ->
|
||||
TrackingMembershipApplyResult = { confirmedProviders ->
|
||||
LibraryRepository.applyMembershipChanges(
|
||||
item = item,
|
||||
desiredMembership = pickerMembership,
|
||||
desiredMembership = desiredMembership,
|
||||
confirmedRemovalProviders = confirmedProviders,
|
||||
)
|
||||
}.onSuccess { result ->
|
||||
}
|
||||
val completeMembershipUpdate: suspend (TrackingMembershipApplyResult) -> Unit = { result ->
|
||||
showTrackingMembershipRewriteFeedback(result)
|
||||
showLibraryListPicker = false
|
||||
pickerItem = null
|
||||
pickerError = null
|
||||
}.onFailure { error ->
|
||||
pickerError = error.message ?: getString(Res.string.tracking_lists_update_failed)
|
||||
}
|
||||
executeTrackingMembershipOperation(
|
||||
operation = { applyMembership(emptySet()) },
|
||||
onSuccess = { result ->
|
||||
if (result.requiresRemovalConfirmation) {
|
||||
pendingTrackingRemoval = PendingTrackingMembershipRemoval(
|
||||
itemTitle = item.name,
|
||||
confirmations = result.requiredRemovalConfirmations,
|
||||
retry = applyMembership,
|
||||
onApplied = completeMembershipUpdate,
|
||||
onFailure = { error ->
|
||||
pickerError = error.message ?: trackingListsUpdateFailedMessage
|
||||
},
|
||||
)
|
||||
} else {
|
||||
completeMembershipUpdate(result)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
pickerError = error.message ?: trackingListsUpdateFailedMessage
|
||||
},
|
||||
)
|
||||
pickerPending = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
TrackingMembershipRemovalConfirmationHost(
|
||||
pending = pendingTrackingRemoval,
|
||||
onPendingChange = { pendingTrackingRemoval = it },
|
||||
)
|
||||
|
||||
NuvioStatusModal(
|
||||
title = stringResource(Res.string.app_exit_title),
|
||||
message = stringResource(Res.string.app_exit_message),
|
||||
|
|
|
|||
|
|
@ -87,10 +87,20 @@ object PosterZoomAnchorHolder {
|
|||
fun consume(): PosterZoomAnchor? = pending.also { pending = null }
|
||||
}
|
||||
|
||||
enum class PosterZoomOverlayExitAnimation {
|
||||
COLLAPSE,
|
||||
DISINTEGRATE,
|
||||
}
|
||||
|
||||
class PosterZoomOverlayAction(
|
||||
val icon: ImageVector,
|
||||
val label: String,
|
||||
val isDestructive: Boolean = false,
|
||||
val exitAnimation: PosterZoomOverlayExitAnimation = if (isDestructive) {
|
||||
PosterZoomOverlayExitAnimation.DISINTEGRATE
|
||||
} else {
|
||||
PosterZoomOverlayExitAnimation.COLLAPSE
|
||||
},
|
||||
val onSelected: () -> Unit,
|
||||
)
|
||||
|
||||
|
|
@ -181,7 +191,7 @@ fun NuvioPosterZoomActionOverlay(
|
|||
|
||||
fun select(action: PosterZoomOverlayAction) {
|
||||
if (phase != PosterZoomPhase.Open) return
|
||||
if (action.isDestructive) {
|
||||
if (action.exitAnimation == PosterZoomOverlayExitAnimation.DISINTEGRATE) {
|
||||
phase = PosterZoomPhase.Disintegrating
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
action.onSelected()
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ import com.nuvio.app.core.i18n.localizedSeasonEpisodeCode
|
|||
import com.nuvio.app.core.ui.NuvioBackButton
|
||||
import com.nuvio.app.core.ui.NuvioCardDepthSurface
|
||||
import com.nuvio.app.core.ui.NuvioPosterZoomActionOverlay
|
||||
import com.nuvio.app.core.ui.NuvioToastController
|
||||
import com.nuvio.app.core.ui.PosterZoomAnchor
|
||||
import com.nuvio.app.core.ui.PosterZoomAnchorHolder
|
||||
import com.nuvio.app.core.ui.PosterZoomOverlayAction
|
||||
|
|
@ -106,6 +107,9 @@ import com.nuvio.app.features.details.components.SeasonWatchedActionSheet
|
|||
import com.nuvio.app.features.details.components.TrailerPlayerPopup
|
||||
import com.nuvio.app.features.home.MetaPreview
|
||||
import com.nuvio.app.features.library.LibraryRepository
|
||||
import com.nuvio.app.features.library.PendingTrackingMembershipRemoval
|
||||
import com.nuvio.app.features.library.TrackingMembershipRemovalConfirmationHost
|
||||
import com.nuvio.app.features.library.executeTrackingMembershipOperation
|
||||
import com.nuvio.app.features.library.showTrackingMembershipRewriteFeedback
|
||||
import com.nuvio.app.features.library.toLibraryItem
|
||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||
|
|
@ -118,6 +122,7 @@ 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.tracking.TrackingLibraryTab
|
||||
import com.nuvio.app.features.tracking.TrackingMembershipApplyResult
|
||||
import com.nuvio.app.features.tracking.toggleTrackingLibraryMembership
|
||||
import com.nuvio.app.features.tracking.TrackingSettingsRepository
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
|
|
@ -224,6 +229,10 @@ fun MetaDetailsScreen(
|
|||
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) }
|
||||
var pendingTrackingRemoval by remember(type, id) {
|
||||
mutableStateOf<PendingTrackingMembershipRemoval?>(null)
|
||||
}
|
||||
val trackingListsUpdateFailedMessage = stringResource(Res.string.tracking_lists_update_failed)
|
||||
var episodeImdbRatings by remember(type, id) { mutableStateOf<Map<Pair<Int, Int>, Double>>(emptyMap()) }
|
||||
var deferredMetaWorkAllowed by remember(type, id) { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -507,9 +516,44 @@ fun MetaDetailsScreen(
|
|||
Unit
|
||||
}
|
||||
}
|
||||
val toggleSaved = remember(meta) {
|
||||
val toggleSaved = remember(meta, trackingListsUpdateFailedMessage) {
|
||||
{
|
||||
LibraryRepository.toggleSaved(meta.toLibraryItem(savedAtEpochMs = 0L))
|
||||
val item = meta.toLibraryItem(savedAtEpochMs = 0L)
|
||||
detailsScope.launch {
|
||||
val toggleMembership: suspend (Set<TrackingProviderId>) ->
|
||||
TrackingMembershipApplyResult = { confirmedProviders ->
|
||||
LibraryRepository.toggleSaved(
|
||||
item = item,
|
||||
confirmedRemovalProviders = confirmedProviders,
|
||||
)
|
||||
}
|
||||
executeTrackingMembershipOperation(
|
||||
operation = { toggleMembership(emptySet()) },
|
||||
onSuccess = { result ->
|
||||
if (result.requiresRemovalConfirmation) {
|
||||
pendingTrackingRemoval = PendingTrackingMembershipRemoval(
|
||||
itemTitle = item.name,
|
||||
confirmations = result.requiredRemovalConfirmations,
|
||||
retry = toggleMembership,
|
||||
onApplied = ::showTrackingMembershipRewriteFeedback,
|
||||
onFailure = { error ->
|
||||
NuvioToastController.show(
|
||||
error.message ?: trackingListsUpdateFailedMessage,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
showTrackingMembershipRewriteFeedback(result)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
NuvioToastController.show(
|
||||
error.message ?: trackingListsUpdateFailedMessage,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
Unit
|
||||
}
|
||||
}
|
||||
val toggleWatched = remember(metaPreview) {
|
||||
|
|
@ -1305,22 +1349,52 @@ fun MetaDetailsScreen(
|
|||
detailsScope.launch {
|
||||
pickerPending = true
|
||||
pickerError = null
|
||||
runCatching {
|
||||
val item = meta.toLibraryItem(savedAtEpochMs = 0L)
|
||||
val desiredMembership = pickerMembership.toMap()
|
||||
val applyMembership: suspend (Set<TrackingProviderId>) ->
|
||||
TrackingMembershipApplyResult = { confirmedProviders ->
|
||||
LibraryRepository.applyMembershipChanges(
|
||||
item = meta.toLibraryItem(savedAtEpochMs = 0L),
|
||||
desiredMembership = pickerMembership,
|
||||
item = item,
|
||||
desiredMembership = desiredMembership,
|
||||
confirmedRemovalProviders = confirmedProviders,
|
||||
)
|
||||
}.onSuccess { result ->
|
||||
}
|
||||
val completeMembershipUpdate: suspend (TrackingMembershipApplyResult) -> Unit = { result ->
|
||||
showTrackingMembershipRewriteFeedback(result)
|
||||
showLibraryListPicker = false
|
||||
}.onFailure { error ->
|
||||
pickerError = error.message ?: getString(Res.string.tracking_lists_update_failed)
|
||||
}
|
||||
executeTrackingMembershipOperation(
|
||||
operation = { applyMembership(emptySet()) },
|
||||
onSuccess = { result ->
|
||||
if (result.requiresRemovalConfirmation) {
|
||||
pendingTrackingRemoval = PendingTrackingMembershipRemoval(
|
||||
itemTitle = item.name,
|
||||
confirmations = result.requiredRemovalConfirmations,
|
||||
retry = applyMembership,
|
||||
onApplied = completeMembershipUpdate,
|
||||
onFailure = { error ->
|
||||
pickerError = error.message
|
||||
?: trackingListsUpdateFailedMessage
|
||||
},
|
||||
)
|
||||
} else {
|
||||
completeMembershipUpdate(result)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
pickerError = error.message ?: trackingListsUpdateFailedMessage
|
||||
},
|
||||
)
|
||||
pickerPending = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
TrackingMembershipRemovalConfirmationHost(
|
||||
pending = pendingTrackingRemoval,
|
||||
onPendingChange = { pendingTrackingRemoval = it },
|
||||
)
|
||||
|
||||
selectedComment?.let { comment ->
|
||||
val commentIndex = comments.indexOfFirst { it.id == comment.id }.coerceAtLeast(0)
|
||||
CommentDetailSheet(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.nuvio.app.core.auth.AuthState
|
|||
import com.nuvio.app.core.network.SupabaseProvider
|
||||
import com.nuvio.app.core.sync.putSyncOriginClientId
|
||||
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.tracking.TrackingLibraryProvider
|
||||
|
|
@ -14,6 +13,7 @@ import com.nuvio.app.features.tracking.TrackingLibraryTab
|
|||
import com.nuvio.app.features.tracking.TrackingLibraryTabKind
|
||||
import com.nuvio.app.features.tracking.TrackingMembershipApplyResult
|
||||
import com.nuvio.app.features.tracking.TrackingMembershipResolution
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import com.nuvio.app.features.tracking.TrackingProviderRegistry
|
||||
import com.nuvio.app.features.tracking.TrackingRefreshIntent
|
||||
import com.nuvio.app.features.tracking.TrackingSettingsRepository
|
||||
|
|
@ -51,7 +51,6 @@ 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.tracking_lists_update_failed
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
|
|
@ -272,30 +271,37 @@ object LibraryRepository {
|
|||
private fun isActiveOperation(token: LibraryProfileToken): Boolean =
|
||||
localState.isCurrent(token) && ProfileRepository.activeProfileId == token.profileId
|
||||
|
||||
fun toggleSaved(item: LibraryItem) {
|
||||
suspend fun toggleSaved(
|
||||
item: LibraryItem,
|
||||
confirmedRemovalProviders: Set<TrackingProviderId> = emptySet(),
|
||||
): TrackingMembershipApplyResult {
|
||||
ensureLoaded()
|
||||
|
||||
activeLibraryProvider()?.let { provider ->
|
||||
val profileId = localState.snapshot().token.profileId
|
||||
val providerMembership = provider.membership(item)
|
||||
val desiredMembership = provider.toggledDefaultMembership(providerMembership)
|
||||
log.i {
|
||||
"toggleSaved routed to ${provider.providerId.storageId} library source " +
|
||||
"item=${item.id} type=${item.type} profile=$profileId"
|
||||
"item=${item.id} type=${item.type} profile=${localState.snapshot().token.profileId}"
|
||||
}
|
||||
syncScope.launch {
|
||||
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(
|
||||
error.message?.takeIf(String::isNotBlank)
|
||||
?: getString(Res.string.tracking_lists_update_failed),
|
||||
)
|
||||
}
|
||||
publish()
|
||||
}
|
||||
return
|
||||
return applyMembershipChanges(
|
||||
item = item,
|
||||
desiredMembership = desiredMembership,
|
||||
confirmedRemovalProviders = confirmedRemovalProviders,
|
||||
targetProviderIds = setOf(provider.providerId),
|
||||
updateLocal = false,
|
||||
)
|
||||
}
|
||||
|
||||
return toggleLocalSavedInternal(item)
|
||||
}
|
||||
|
||||
fun toggleLocalSaved(item: LibraryItem) {
|
||||
ensureLoaded()
|
||||
toggleLocalSavedInternal(item)
|
||||
}
|
||||
|
||||
private fun toggleLocalSavedInternal(item: LibraryItem): TrackingMembershipApplyResult {
|
||||
val result = localState.toggle(
|
||||
item.copy(savedAtEpochMs = LibraryClock.nowEpochMs()),
|
||||
)
|
||||
|
|
@ -313,6 +319,7 @@ object LibraryRepository {
|
|||
persist(result.snapshot)
|
||||
publish()
|
||||
pushToServer(result.snapshot)
|
||||
return TrackingMembershipApplyResult()
|
||||
}
|
||||
|
||||
fun save(item: LibraryItem) {
|
||||
|
|
@ -392,17 +399,50 @@ object LibraryRepository {
|
|||
suspend fun applyMembershipChanges(
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
confirmedRemovalProviders: Set<TrackingProviderId> = emptySet(),
|
||||
): TrackingMembershipApplyResult = applyMembershipChanges(
|
||||
item = item,
|
||||
desiredMembership = desiredMembership,
|
||||
confirmedRemovalProviders = confirmedRemovalProviders,
|
||||
targetProviderIds = null,
|
||||
updateLocal = true,
|
||||
)
|
||||
|
||||
private suspend fun applyMembershipChanges(
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
confirmedRemovalProviders: Set<TrackingProviderId>,
|
||||
targetProviderIds: Set<TrackingProviderId>?,
|
||||
updateLocal: Boolean,
|
||||
): TrackingMembershipApplyResult {
|
||||
ensureLoaded()
|
||||
val localDesired = desiredMembership[LOCAL_LIBRARY_LIST_KEY] == true
|
||||
val currentlyInLocal = localState.contains(item.id, item.type)
|
||||
val profileId = localState.snapshot().token.profileId
|
||||
val providerChanges = TrackingProviderRegistry.connectedLibraryProviders()
|
||||
.filter { provider -> targetProviderIds == null || provider.providerId in targetProviderIds }
|
||||
.mapNotNull { provider ->
|
||||
val providerListKeys = provider.snapshot().tabs.mapTo(mutableSetOf(), TrackingLibraryTab::key)
|
||||
val providerMembership = desiredMembership.filterKeys(providerListKeys::contains)
|
||||
providerMembership.takeIf { membership -> membership.isNotEmpty() }?.let { membership ->
|
||||
provider to membership
|
||||
}
|
||||
}
|
||||
val requiredConfirmations = providerChanges.mapNotNull { (provider, providerMembership) ->
|
||||
provider.membershipRemovalConfirmation(item, providerMembership)
|
||||
?.takeUnless { confirmation -> confirmation.providerId in confirmedRemovalProviders }
|
||||
}
|
||||
log.i {
|
||||
"Applying library membership item=${item.id} type=${item.type} profile=$profileId " +
|
||||
"localDesired=$localDesired currentlyInLocal=$currentlyInLocal " +
|
||||
"connectedProviders=${TrackingProviderRegistry.connectedProviderIdsSnapshot()}"
|
||||
}
|
||||
if (localDesired != currentlyInLocal) {
|
||||
if (requiredConfirmations.isNotEmpty()) {
|
||||
return TrackingMembershipApplyResult(
|
||||
requiredRemovalConfirmations = requiredConfirmations,
|
||||
)
|
||||
}
|
||||
if (updateLocal && localDesired != currentlyInLocal) {
|
||||
if (localDesired) {
|
||||
save(item)
|
||||
} else {
|
||||
|
|
@ -412,22 +452,19 @@ object LibraryRepository {
|
|||
|
||||
var firstFailure: Throwable? = null
|
||||
val resolutions = mutableListOf<TrackingMembershipResolution>()
|
||||
TrackingProviderRegistry.connectedLibraryProviders().forEach { provider ->
|
||||
val providerListKeys = provider.snapshot().tabs.mapTo(mutableSetOf(), TrackingLibraryTab::key)
|
||||
val providerMembership = desiredMembership.filterKeys(providerListKeys::contains)
|
||||
if (providerMembership.isNotEmpty()) {
|
||||
try {
|
||||
provider.applyMembership(
|
||||
profileId = profileId,
|
||||
item = item,
|
||||
desiredMembership = providerMembership,
|
||||
)?.let(resolutions::add)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
if (firstFailure == null) firstFailure = error
|
||||
log.e(error) { "Failed to update ${provider.providerId.storageId} library membership" }
|
||||
}
|
||||
providerChanges.forEach { (provider, providerMembership) ->
|
||||
try {
|
||||
provider.applyMembership(
|
||||
profileId = profileId,
|
||||
item = item,
|
||||
desiredMembership = providerMembership,
|
||||
destructiveRemovalConfirmed = provider.providerId in confirmedRemovalProviders,
|
||||
)?.let(resolutions::add)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
if (firstFailure == null) firstFailure = error
|
||||
log.e(error) { "Failed to update ${provider.providerId.storageId} library membership" }
|
||||
}
|
||||
}
|
||||
publish()
|
||||
|
|
@ -435,12 +472,30 @@ object LibraryRepository {
|
|||
return TrackingMembershipApplyResult(resolutions = resolutions)
|
||||
}
|
||||
|
||||
suspend fun removeFromList(item: LibraryItem, listKey: String) {
|
||||
suspend fun removeFromList(
|
||||
item: LibraryItem,
|
||||
listKey: String,
|
||||
confirmedRemovalProviders: Set<TrackingProviderId> = emptySet(),
|
||||
): TrackingMembershipApplyResult {
|
||||
ensureLoaded()
|
||||
val targetProvider = TrackingProviderRegistry.connectedLibraryProviders()
|
||||
.firstOrNull { provider -> provider.snapshot().tabs.any { tab -> tab.key == listKey } }
|
||||
val currentMembership = if (listKey == LOCAL_LIBRARY_LIST_KEY) {
|
||||
mapOf(LOCAL_LIBRARY_LIST_KEY to localState.contains(item.id, item.type))
|
||||
} else {
|
||||
targetProvider?.membership(item).orEmpty()
|
||||
}
|
||||
val desiredMembership = libraryMembershipWithRemovedList(
|
||||
currentMembership = getMembershipSnapshot(item),
|
||||
currentMembership = currentMembership,
|
||||
listKey = listKey,
|
||||
)
|
||||
applyMembershipChanges(item, desiredMembership)
|
||||
return applyMembershipChanges(
|
||||
item = item,
|
||||
desiredMembership = desiredMembership,
|
||||
confirmedRemovalProviders = confirmedRemovalProviders,
|
||||
targetProviderIds = targetProvider?.let { provider -> setOf(provider.providerId) }.orEmpty(),
|
||||
updateLocal = listKey == LOCAL_LIBRARY_LIST_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
private fun pushToServer(snapshot: LibraryLocalSnapshot) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
package com.nuvio.app.features.library
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.nuvio.app.core.ui.NuvioStatusModal
|
||||
import com.nuvio.app.features.tracking.TrackingMembershipApplyResult
|
||||
import com.nuvio.app.features.tracking.TrackingMembershipRemovalConfirmation
|
||||
import com.nuvio.app.features.tracking.TrackingMembershipRemovalImpact
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import com.nuvio.app.features.tracking.TrackingProviderRegistry
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.action_cancel
|
||||
import nuvio.composeapp.generated.resources.action_remove_anyway
|
||||
import nuvio.composeapp.generated.resources.tracking_remove_confirmation_message
|
||||
import nuvio.composeapp.generated.resources.tracking_remove_confirmation_title
|
||||
import nuvio.composeapp.generated.resources.tracking_removal_impact_history
|
||||
import nuvio.composeapp.generated.resources.tracking_removal_impact_history_and_rating
|
||||
import nuvio.composeapp.generated.resources.tracking_removal_impact_rating
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
class PendingTrackingMembershipRemoval(
|
||||
val itemTitle: String,
|
||||
val confirmations: List<TrackingMembershipRemovalConfirmation>,
|
||||
val retry: suspend (Set<TrackingProviderId>) -> TrackingMembershipApplyResult,
|
||||
val onApplied: suspend (TrackingMembershipApplyResult) -> Unit,
|
||||
val onFailure: suspend (Throwable) -> Unit,
|
||||
)
|
||||
|
||||
suspend fun executeTrackingMembershipOperation(
|
||||
operation: suspend () -> TrackingMembershipApplyResult,
|
||||
onSuccess: suspend (TrackingMembershipApplyResult) -> Unit,
|
||||
onFailure: suspend (Throwable) -> Unit,
|
||||
) {
|
||||
try {
|
||||
onSuccess(operation())
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
onFailure(error)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TrackingMembershipRemovalConfirmationHost(
|
||||
pending: PendingTrackingMembershipRemoval?,
|
||||
onPendingChange: (PendingTrackingMembershipRemoval?) -> Unit,
|
||||
) {
|
||||
var isBusy by remember(pending) { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val confirmations = pending?.confirmations.orEmpty()
|
||||
val providerNames = confirmations
|
||||
.map { confirmation ->
|
||||
TrackingProviderRegistry.authProvider(confirmation.providerId)
|
||||
?.descriptor
|
||||
?.displayName
|
||||
?: confirmation.providerId.storageId.replaceFirstChar(Char::uppercase)
|
||||
}
|
||||
.distinct()
|
||||
.joinToString()
|
||||
val impacts = confirmations.flatMapTo(linkedSetOf()) { confirmation -> confirmation.impacts }
|
||||
val impactLabel = when (impacts) {
|
||||
setOf(TrackingMembershipRemovalImpact.WATCHED_HISTORY) ->
|
||||
stringResource(Res.string.tracking_removal_impact_history)
|
||||
|
||||
setOf(TrackingMembershipRemovalImpact.RATING) ->
|
||||
stringResource(Res.string.tracking_removal_impact_rating)
|
||||
|
||||
else -> stringResource(Res.string.tracking_removal_impact_history_and_rating)
|
||||
}
|
||||
|
||||
NuvioStatusModal(
|
||||
title = stringResource(Res.string.tracking_remove_confirmation_title, providerNames),
|
||||
message = stringResource(
|
||||
Res.string.tracking_remove_confirmation_message,
|
||||
pending?.itemTitle.orEmpty(),
|
||||
providerNames,
|
||||
impactLabel,
|
||||
),
|
||||
isVisible = pending != null,
|
||||
isBusy = isBusy,
|
||||
confirmText = stringResource(Res.string.action_remove_anyway),
|
||||
dismissText = stringResource(Res.string.action_cancel),
|
||||
onConfirm = {
|
||||
val request = pending ?: return@NuvioStatusModal
|
||||
if (isBusy) return@NuvioStatusModal
|
||||
isBusy = true
|
||||
scope.launch {
|
||||
try {
|
||||
val confirmedProviders = request.confirmations.mapTo(linkedSetOf()) { confirmation ->
|
||||
confirmation.providerId
|
||||
}
|
||||
val result = request.retry(confirmedProviders)
|
||||
if (result.requiresRemovalConfirmation) {
|
||||
onPendingChange(
|
||||
PendingTrackingMembershipRemoval(
|
||||
itemTitle = request.itemTitle,
|
||||
confirmations = result.requiredRemovalConfirmations,
|
||||
retry = request.retry,
|
||||
onApplied = request.onApplied,
|
||||
onFailure = request.onFailure,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
onPendingChange(null)
|
||||
request.onApplied(result)
|
||||
}
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
onPendingChange(null)
|
||||
request.onFailure(error)
|
||||
} finally {
|
||||
isBusy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = {
|
||||
if (!isBusy) onPendingChange(null)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ 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.TrackingMembershipRemovalConfirmation
|
||||
import com.nuvio.app.features.tracking.TrackingMembershipRemovalImpact
|
||||
import com.nuvio.app.features.tracking.TrackingMembershipResolution
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import com.nuvio.app.features.tracking.TrackingRefreshIntent
|
||||
|
|
@ -68,6 +70,7 @@ object SimklLibraryRepository {
|
|||
profileId: Int,
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
destructiveRemovalConfirmed: Boolean = false,
|
||||
): TrackingMembershipResolution? {
|
||||
if (profileId != ProfileRepository.activeProfileId) return null
|
||||
ensureLoaded()
|
||||
|
|
@ -98,7 +101,10 @@ object SimklLibraryRepository {
|
|||
)
|
||||
|
||||
currentStatus != null -> {
|
||||
require(snapshot.canSafelyRemoveFromSimklLibrary(item.id)) {
|
||||
require(
|
||||
snapshot.membershipRemovalConfirmation(item.id) == null ||
|
||||
destructiveRemovalConfirmed,
|
||||
) {
|
||||
"Removing this item from Simkl would also clear watched history or a rating"
|
||||
}
|
||||
SimklMutationRepository.removeFromList(profileId = profileId, items = listOf(media))
|
||||
|
|
@ -125,6 +131,20 @@ object SimklLibraryRepository {
|
|||
return resolution
|
||||
}
|
||||
|
||||
fun membershipRemovalConfirmation(
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
): TrackingMembershipRemovalConfirmation? {
|
||||
ensureLoaded()
|
||||
val removesStatus = simklLibraryStatusDefinitions.none { definition ->
|
||||
desiredMembership[definition.key] == true
|
||||
} && findItem(item.id, item.type)?.listKeys.orEmpty().any { key ->
|
||||
simklLibraryStatusDefinition(key) != null
|
||||
}
|
||||
if (!removesStatus) return null
|
||||
return SimklSyncRepository.state.value.snapshot.membershipRemovalConfirmation(item.id)
|
||||
}
|
||||
|
||||
private fun publish(syncState: SimklSyncUiState) {
|
||||
val projection = syncState.snapshot.toSimklLibraryProjection()
|
||||
_uiState.value = SimklLibraryUiState(
|
||||
|
|
@ -191,39 +211,52 @@ object SimklTrackingLibraryProvider : TrackingLibraryProvider {
|
|||
override suspend fun membership(item: LibraryItem): Map<String, Boolean> =
|
||||
SimklLibraryRepository.statusMembership(item.id, item.type)
|
||||
|
||||
override fun toggledDefaultMembership(
|
||||
currentMembership: Map<String, Boolean>,
|
||||
): Map<String, Boolean> = currentMembership.mapValues { false }.toMutableMap().apply {
|
||||
if (currentMembership.values.none { isSelected -> isSelected }) {
|
||||
this[simklLibraryStatusDefinitions.single { definition ->
|
||||
definition.status == SimklListStatus.PLAN_TO_WATCH
|
||||
}.key] = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun membershipRemovalConfirmation(
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
): TrackingMembershipRemovalConfirmation? =
|
||||
SimklLibraryRepository.membershipRemovalConfirmation(item, desiredMembership)
|
||||
|
||||
override suspend fun applyMembership(
|
||||
profileId: Int,
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
destructiveRemovalConfirmed: Boolean,
|
||||
): TrackingMembershipResolution? =
|
||||
SimklLibraryRepository.applyStatusMembership(
|
||||
profileId = profileId,
|
||||
item = item,
|
||||
desiredMembership = desiredMembership,
|
||||
destructiveRemovalConfirmed = destructiveRemovalConfirmed,
|
||||
)
|
||||
|
||||
override suspend fun toggleDefaultMembership(profileId: Int, item: LibraryItem) {
|
||||
val current = SimklLibraryRepository.statusMembership(item.id, item.type)
|
||||
val desired = current.mapValues { false }.toMutableMap()
|
||||
if (current.values.none { isSelected -> isSelected }) {
|
||||
desired[simklLibraryStatusDefinitions.single { definition ->
|
||||
definition.status == SimklListStatus.PLAN_TO_WATCH
|
||||
}.key] = true
|
||||
}
|
||||
SimklLibraryRepository.applyStatusMembership(
|
||||
profileId = profileId,
|
||||
item = item,
|
||||
desiredMembership = desired,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SimklSyncSnapshot.canSafelyRemoveFromSimklLibrary(contentId: String): Boolean =
|
||||
internal fun SimklSyncSnapshot.membershipRemovalConfirmation(
|
||||
contentId: String,
|
||||
): TrackingMembershipRemovalConfirmation? =
|
||||
entries.firstOrNull { entry -> entry.media?.canonicalContentId().equals(contentId, ignoreCase = true) }
|
||||
?.let { entry ->
|
||||
?.takeUnless { entry ->
|
||||
entry.status == SimklListStatus.PLAN_TO_WATCH &&
|
||||
entry.lastWatchedAt == null &&
|
||||
entry.userRating == null &&
|
||||
entry.seasons.none { season -> season.episodes.any { episode -> episode.watchedAt != null } }
|
||||
}
|
||||
?: true
|
||||
?.let {
|
||||
TrackingMembershipRemovalConfirmation(
|
||||
providerId = TrackingProviderId.SIMKL,
|
||||
impacts = setOf(
|
||||
TrackingMembershipRemovalImpact.WATCHED_HISTORY,
|
||||
TrackingMembershipRemovalImpact.RATING,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,23 @@ data class TrackingMembershipResolution(
|
|||
get() = requestedListKey != resolvedListKey
|
||||
}
|
||||
|
||||
enum class TrackingMembershipRemovalImpact {
|
||||
WATCHED_HISTORY,
|
||||
RATING,
|
||||
}
|
||||
|
||||
data class TrackingMembershipRemovalConfirmation(
|
||||
val providerId: TrackingProviderId,
|
||||
val impacts: Set<TrackingMembershipRemovalImpact>,
|
||||
)
|
||||
|
||||
data class TrackingMembershipApplyResult(
|
||||
val resolutions: List<TrackingMembershipResolution> = emptyList(),
|
||||
val requiredRemovalConfirmations: List<TrackingMembershipRemovalConfirmation> = emptyList(),
|
||||
) {
|
||||
val rewrites: List<TrackingMembershipResolution>
|
||||
get() = resolutions.filter(TrackingMembershipResolution::wasRewritten)
|
||||
|
||||
val requiresRemovalConfirmation: Boolean
|
||||
get() = requiredRemovalConfirmations.isNotEmpty()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,12 +89,17 @@ interface TrackingLibraryProvider {
|
|||
fun contains(contentId: String, contentType: String? = null): Boolean
|
||||
fun find(contentId: String): LibraryItem?
|
||||
suspend fun membership(item: LibraryItem): Map<String, Boolean>
|
||||
fun toggledDefaultMembership(currentMembership: Map<String, Boolean>): Map<String, Boolean>
|
||||
fun membershipRemovalConfirmation(
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
): TrackingMembershipRemovalConfirmation? = null
|
||||
suspend fun applyMembership(
|
||||
profileId: Int,
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
destructiveRemovalConfirmed: Boolean = false,
|
||||
): TrackingMembershipResolution?
|
||||
suspend fun toggleDefaultMembership(profileId: Int, item: LibraryItem)
|
||||
}
|
||||
|
||||
/** Provider adapter for watched-history projection and explicit history mutations. */
|
||||
|
|
|
|||
|
|
@ -197,16 +197,6 @@ object TraktLibraryRepository {
|
|||
return TraktMembershipSnapshot(listMembership = map)
|
||||
}
|
||||
|
||||
suspend fun toggleWatchlist(item: LibraryItem) {
|
||||
ensureLoaded()
|
||||
val snapshot = getMembershipSnapshot(item)
|
||||
val currentlyInWatchlist = snapshot.listMembership[WATCHLIST_KEY] == true
|
||||
val desired = snapshot.listMembership.toMutableMap().apply {
|
||||
this[WATCHLIST_KEY] = !currentlyInWatchlist
|
||||
}
|
||||
applyMembershipChanges(item, TraktMembershipChanges(desiredMembership = desired))
|
||||
}
|
||||
|
||||
suspend fun applyMembershipChanges(item: LibraryItem, changes: TraktMembershipChanges) {
|
||||
ensureLoaded()
|
||||
val headers = TraktAuthRepository.authorizedHeaders() ?: return
|
||||
|
|
|
|||
|
|
@ -66,10 +66,21 @@ object TraktTrackingLibraryProvider : TrackingLibraryProvider {
|
|||
override suspend fun membership(item: LibraryItem): Map<String, Boolean> =
|
||||
TraktLibraryRepository.getMembershipSnapshot(item).listMembership
|
||||
|
||||
override fun toggledDefaultMembership(
|
||||
currentMembership: Map<String, Boolean>,
|
||||
): Map<String, Boolean> {
|
||||
val watchlistKey = snapshot().tabs
|
||||
.firstOrNull { tab -> tab.kind == TrackingLibraryTabKind.WATCHLIST }
|
||||
?.key
|
||||
?: return currentMembership
|
||||
return toggledTraktWatchlistMembership(currentMembership, watchlistKey)
|
||||
}
|
||||
|
||||
override suspend fun applyMembership(
|
||||
profileId: Int,
|
||||
item: LibraryItem,
|
||||
desiredMembership: Map<String, Boolean>,
|
||||
destructiveRemovalConfirmed: Boolean,
|
||||
): TrackingMembershipResolution? {
|
||||
TraktLibraryRepository.applyMembershipChanges(
|
||||
item = item,
|
||||
|
|
@ -77,9 +88,13 @@ object TraktTrackingLibraryProvider : TrackingLibraryProvider {
|
|||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun toggleDefaultMembership(profileId: Int, item: LibraryItem) =
|
||||
TraktLibraryRepository.toggleWatchlist(item)
|
||||
internal fun toggledTraktWatchlistMembership(
|
||||
currentMembership: Map<String, Boolean>,
|
||||
watchlistKey: String,
|
||||
): Map<String, Boolean> = currentMembership.toMutableMap().apply {
|
||||
this[watchlistKey] = currentMembership[watchlistKey] != true
|
||||
}
|
||||
|
||||
private fun TraktListTab.toTrackingLibraryTab(): TrackingLibraryTab =
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.nuvio.app.features.simkl
|
||||
|
||||
import com.nuvio.app.features.tracking.TrackingMediaKind
|
||||
import com.nuvio.app.features.tracking.TrackingMembershipRemovalImpact
|
||||
import com.nuvio.app.features.watchprogress.WatchProgressSourceSimklPlayback
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
|
@ -233,6 +234,95 @@ class SimklProjectionsTest {
|
|||
assertEquals(4, reference.episode?.number)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clean plan to watch removal needs no destructive confirmation`() {
|
||||
val plan = entry(
|
||||
type = SimklMediaType.MOVIES,
|
||||
status = SimklListStatus.PLAN_TO_WATCH,
|
||||
id = 53536,
|
||||
imdb = "tt0181852",
|
||||
)
|
||||
|
||||
val confirmation = SimklSyncSnapshot(entries = listOf(plan))
|
||||
.membershipRemovalConfirmation("tt0181852")
|
||||
|
||||
assertNull(confirmation)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `watched or rated Simkl removal requires destructive confirmation`() {
|
||||
val watchedPlan = entry(
|
||||
type = SimklMediaType.MOVIES,
|
||||
status = SimklListStatus.PLAN_TO_WATCH,
|
||||
id = 53536,
|
||||
imdb = "tt0181852",
|
||||
lastWatchedAt = "2023-11-14T22:13:20Z",
|
||||
)
|
||||
val ratedPlan = entry(
|
||||
type = SimklMediaType.MOVIES,
|
||||
status = SimklListStatus.PLAN_TO_WATCH,
|
||||
id = 53434,
|
||||
imdb = "tt0068646",
|
||||
).copy(userRating = 9)
|
||||
val episodePlan = entry(
|
||||
type = SimklMediaType.SHOWS,
|
||||
status = SimklListStatus.PLAN_TO_WATCH,
|
||||
id = 2090,
|
||||
imdb = "tt1520211",
|
||||
seasons = listOf(
|
||||
SimklSeason(
|
||||
number = 1,
|
||||
episodes = listOf(
|
||||
SimklEpisode(number = 1, watchedAt = "2023-11-14T22:13:20Z"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val confirmation = assertNotNull(
|
||||
SimklSyncSnapshot(entries = listOf(watchedPlan, ratedPlan, episodePlan))
|
||||
.membershipRemovalConfirmation("tt0181852"),
|
||||
)
|
||||
assertNotNull(
|
||||
SimklSyncSnapshot(entries = listOf(watchedPlan, ratedPlan, episodePlan))
|
||||
.membershipRemovalConfirmation("tt0068646"),
|
||||
)
|
||||
assertNotNull(
|
||||
SimklSyncSnapshot(entries = listOf(watchedPlan, ratedPlan, episodePlan))
|
||||
.membershipRemovalConfirmation("tt1520211"),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
setOf(
|
||||
TrackingMembershipRemovalImpact.WATCHED_HISTORY,
|
||||
TrackingMembershipRemovalImpact.RATING,
|
||||
),
|
||||
confirmation.impacts,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Simkl default membership toggles only its mutually exclusive status`() {
|
||||
val planKey = simklLibraryStatusDefinitions.single { definition ->
|
||||
definition.status == SimklListStatus.PLAN_TO_WATCH
|
||||
}.key
|
||||
val watchingKey = simklLibraryStatusDefinitions.single { definition ->
|
||||
definition.status == SimklListStatus.WATCHING
|
||||
}.key
|
||||
val emptyMembership = simklLibraryStatusDefinitions.associate { definition ->
|
||||
definition.key to false
|
||||
}
|
||||
|
||||
val added = SimklTrackingLibraryProvider.toggledDefaultMembership(emptyMembership)
|
||||
val removed = SimklTrackingLibraryProvider.toggledDefaultMembership(
|
||||
emptyMembership + (watchingKey to true),
|
||||
)
|
||||
|
||||
assertTrue(added[planKey] == true)
|
||||
assertTrue(added.filterKeys { key -> key != planKey }.values.none { it })
|
||||
assertTrue(removed.values.none { it })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `timestamp parser accepts UTC fractions and rejects invalid calendar values`() {
|
||||
assertEquals(0L, parseSimklUtcEpochMs("1970-01-01T00:00:00Z"))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.nuvio.app.features.trakt
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class TraktTrackingLibraryProviderTest {
|
||||
@Test
|
||||
fun `default toggle changes only watchlist membership`() {
|
||||
val membership = mapOf(
|
||||
"trakt:watchlist" to false,
|
||||
"trakt:list:42" to true,
|
||||
)
|
||||
|
||||
val added = toggledTraktWatchlistMembership(membership, "trakt:watchlist")
|
||||
val removed = toggledTraktWatchlistMembership(added, "trakt:watchlist")
|
||||
|
||||
assertEquals(true, added["trakt:watchlist"])
|
||||
assertEquals(true, added["trakt:list:42"])
|
||||
assertEquals(false, removed["trakt:watchlist"])
|
||||
assertEquals(true, removed["trakt:list:42"])
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue