mirror of
https://github.com/yattee/yattee.git
synced 2026-10-28 07:27:18 +00:00
Read a playlist back before writing over it
Ten review findings in the remote-playlist work, all of them the same shape: something acted on what it remembered instead of what the server says now. - A confirmed-gone playlist is forgotten whole — snapshot, list entry, and both ordering fences — so a refresh that started before the delete can no longer republish it, and a list refetch that fails offline no longer leaves it on screen. - `.accountsDidChange` discards account-scoped content when the account it belonged to is no longer the effective one, as the status notification already did. - A tab that stops being offered no longer stays selected: the browse view moves to the first available one, which reloads by itself. - A Handoff account id is accepted only when that account is on the instance the playlist lives on. - Composite writes moved up into `AccountService`, over a transport whose every method is exactly one request: create-with-privacy, rename, and set-description. The credential is rechecked between each pair, so a sign-out between the two halves of a create or an edit can no longer send the second half under the previous user's secret. - Editing a playlist no longer forces it private. Invidious serialises visibility as `isListed` — `privacy.public?`, so unlisted and private are indistinguishable — which the snapshot now carries; the picker starts there, and sends nothing at all unless it was moved. Invidious keeps the stored visibility when the PATCH omits it (measured), so an untouched picker leaves it exactly as it was. - Tapping Edit on a server-side playlist awaits an authenticated refetch first and populates the form from it; a failed refetch shows an error and no sheet. - Playlist rows are keyed by row rather than by video id, which two rows of the same video share. - The add-to-playlist sheet offers only accounts whose instance is enabled and that hold a credential, alongside the write capability.
This commit is contained in:
@@ -446,19 +446,16 @@ struct InstanceBrowseView: View {
|
||||
}
|
||||
// A credential can arrive over iCloud Keychain, or an account be added,
|
||||
// while this stays on screen — the Feed tab depends on it and would
|
||||
// otherwise stay missing until the view was rebuilt. A status change can
|
||||
// also move *which* account is effective here, since the resolution
|
||||
// falls back to a sibling login on the same instance, so the content
|
||||
// loaded for the previous one has to go with it.
|
||||
// otherwise stay missing until the view was rebuilt. Either notification
|
||||
// can also move *which* account is effective here: a status change
|
||||
// because the resolution falls back to a sibling login on the same
|
||||
// instance, an add or a remove because it changes what there is to fall
|
||||
// back to. Content loaded for the previous one has to go with it.
|
||||
.onReceive(NotificationCenter.default.publisher(for: .accountStatusesDidChange)) { _ in
|
||||
let accountChanged = discardAccountScopedContentIfAccountChanged()
|
||||
refreshLoginState()
|
||||
if accountChanged {
|
||||
Task { await startContentLoad() }
|
||||
}
|
||||
handleEffectiveAccountChange()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .accountsDidChange)) { _ in
|
||||
refreshLoginState()
|
||||
handleEffectiveAccountChange()
|
||||
}
|
||||
// Selecting the other login on this instance changes whose feed and
|
||||
// playlists these are. Nothing else here would notice: the instance,
|
||||
@@ -466,14 +463,18 @@ struct InstanceBrowseView: View {
|
||||
.onReceive(NotificationCenter.default.publisher(for: .dataContextDidChange)) { _ in
|
||||
discardAccountScopedContent()
|
||||
refreshLoginState()
|
||||
Task { await startContentLoad() }
|
||||
if !resetSelectedTabIfUnavailable() {
|
||||
Task { await startContentLoad() }
|
||||
}
|
||||
}
|
||||
// Same reason, without the context moving: the credential behind this
|
||||
// instance's account was replaced.
|
||||
.onReceive(NotificationCenter.default.publisher(for: .accountCredentialDidChange)) { _ in
|
||||
discardAccountScopedContent()
|
||||
refreshLoginState()
|
||||
Task { await startContentLoad() }
|
||||
if !resetSelectedTabIfUnavailable() {
|
||||
Task { await startContentLoad() }
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedTab) { _, _ in
|
||||
isLoading = true
|
||||
@@ -1255,6 +1256,37 @@ struct InstanceBrowseView: View {
|
||||
return true
|
||||
}
|
||||
|
||||
/// Handles a change to which account this instance resolves to, whatever
|
||||
/// moved it: a status, or an account being added or removed.
|
||||
private func handleEffectiveAccountChange() {
|
||||
let accountChanged = discardAccountScopedContentIfAccountChanged()
|
||||
refreshLoginState()
|
||||
|
||||
// Moving off a tab that no longer exists reloads by itself.
|
||||
guard !resetSelectedTabIfUnavailable() else { return }
|
||||
|
||||
if accountChanged {
|
||||
Task { await startContentLoad() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves off a tab that has just stopped being offered.
|
||||
///
|
||||
/// The Feed and Playlists tabs exist only while an account backs them. When
|
||||
/// one goes away the picker drops it, and a selection left pointing at it
|
||||
/// leaves an unhighlighted tab strip over content nobody can reach.
|
||||
///
|
||||
/// - Returns: whether the selection moved, which reloads through
|
||||
/// `onChange(of: selectedTab)`.
|
||||
@discardableResult
|
||||
private func resetSelectedTabIfUnavailable() -> Bool {
|
||||
let tabs = availableTabs
|
||||
guard !tabs.contains(selectedTab), let first = tabs.first else { return false }
|
||||
|
||||
selectedTab = first
|
||||
return true
|
||||
}
|
||||
|
||||
private func refreshLoginState() {
|
||||
guard instance.supportsAuthentication else { return }
|
||||
isLoggedIn = appEnvironment?.isSignedIn(to: instance) ?? false
|
||||
|
||||
Reference in New Issue
Block a user