A fresh device never adopted the synced context. The local value is a
default stamped when the manager is built, so it is always newer than a
choice made on another device, and ordering against it discarded that
choice. With no local decision to protect, the first synced context is
simply adopted.
An account arriving mid-session sat at "sign in required" until the app
was relaunched: nothing recomputed its status once the metadata — or its
instance — turned up, even when iCloud Keychain had already delivered
the credential. Both arrivals now recompute statuses and re-run the
legacy mirror, which was similarly stuck whenever the instance had not
arrived yet.
Signing in again keeps the same account and the same context, so nothing
downstream saw a change — but the credential may now belong to somebody
else, and their subscriptions and feed stayed on screen. The provider
and the account's caches are discarded on every credential replacement.
The deletion repair also cancels a context upload the deleted choice had
queued; sending it would have published the fallback under that choice's
timestamp, which not stamping it "now" exists to prevent.
Piped subscribe/unsubscribe and Invidious unsubscribe now report their
outcome like every other authenticated call, so a credential rejected by
one of them stops claiming to be signed in. Removing the last account
updates the source screen, which kept offering authenticated imports
that could only fail.
A deletion can reach a device before the account it deletes does — the
tombstone and the account are separate records, and iCloud Keychain runs
on its own timeline — and the credential cleanup was skipped whenever
the account was unknown here. The scoped key needs only the account ID,
so both credential managers are now asked to drop it; the unscoped
legacy item still needs the instance and is left alone.
Cached data is invalidated on the data context rather than on the legacy
selection it maps to. Two logins on the same instance collapse onto the
same legacy value, so switching between them cleared nothing and the
subscriptions feed kept showing the previous account's videos. Home
feeds are keyed by instance alone and had the same hole across accounts
and across Apple accounts; they are dropped with it.
The third source-deletion path now cascades to accounts like the other
two, and the Invidious private-playlist lookup resolves its session
through the account — a login created after the revamp writes only an
account-scoped credential, so the instance-keyed lookup missed it and
fell through to the public endpoint.
A synced context change had no effect on the pre-revamp
`subscriptionAccount` setting several views still read, nor on the feed
cache, which is global rather than account-keyed — so a selection made
on another device left this one showing the previous account's
subscriptions and videos. Both now follow every context change,
whoever made it.
Instances replaced wholesale by iCloud bypassed `saveInstances()` and so
posted no change notification. A legacy authenticated instance arriving
that way never reached account migration: no account row, and no action
to adopt its saved login, until an unrelated edit or a relaunch.
Deleting an instance now deletes the accounts on it, while the instance
is still resolvable — afterwards the account's credential manager cannot
be determined from its type and the credential would be stranded. Only
an explicit deletion cascades; an instance vanishing from iCloud leaves
its accounts orphaned, because absence is not a deletion.
A context whose instance has been switched off is temporarily
unavailable rather than valid. The account stays signed in, so nothing
about the account said the server had been turned off, and requests kept
going to it while the switcher already hid it.
A login created after the revamp got a random account UUID that never
left the device, so its credential was keyed to an ID no other device
had and the login was unusable there. Migrated accounts were fine only
because their ID is deterministically the instance ID.
Accounts, the permanent legacy-migration markers, the permanent deletion
tombstones and the selected data context now ride the existing
CKSyncEngine as four record types, with the rules the category needs:
- Absence proves nothing. A fetch resumes from a change token and never
enumerates the zone, so deletions are records of their own and they
dominate the account wherever they are seen — including the engine's
retry-as-insert path, which is how a queued offline edit would
otherwise resurrect a deleted account.
- Fetched records are staged durably before each delegate event returns
and applied as a set, tombstones first, gated on the zone's own result
rather than didFetchChanges, which fires after failed fetches too.
- Nothing activates for an identity that has not bootstrapped: a device
left on the previous release consumed these record types as unknown
and advanced its token past them, so each identity fetches the zone
once from a nil token, outside the engine's own token.
- A disabled category defers records rather than discarding them; every
persisted artifact is namespaced by identity, and async work is
generation-checked so a fetch begun under one Apple account can never
land in another's state.
- Uploads are ordered: a marker never outruns the account it describes,
and a context naming an account is never published before that account
is server-confirmed.
Round two of review on the accounts layer.
The settings picker mapped its legacy instance-keyed selection straight
onto DataContext(legacy:), which turns an instance UUID into an account
UUID. That identity only holds for migrated accounts - a login created
after the revamp has a random ID - so selecting a freshly added account
named one that does not exist and left the provider nil. Selections now
resolve through AccountsManager to an actual account on that instance,
preferring a signed-in one. The picker still lists one entry per instance
and so cannot show a second login; the Phase 3 switcher replaces it.
BackgroundFeedRefresher was missed in the previous sweep because it
resolves credentials differently: it routed on the legacy setting and
looked up "first enabled Invidious instance" with an instance-keyed
credential. After any new-style login that silently found nothing and
skipped the refresh. It now routes on the selected context and takes the
credential from that account.
Subscription reads used the raw stored context, so an account waiting on
its instance or credential rendered empty instead of falling back to local
data as the tri-state design specifies. Reads go through the validator's
effective context; writes deliberately do not, so a subscribe fails
visibly rather than landing in local data the user did not choose.
Account statuses are cleared when the identity namespace reloads, but
nothing recomputed them once a deferred scope resolution succeeded - a
synced account kept showing "sign in required" until relaunch. The scope
change now triggers a refresh, deferred by one turn so it cannot depend on
observer registration order relative to the store's own reload.
Five findings against the accounts layer, all real.
A fresh login created a random-UUID account and then flipped
usesAccountLogin, which posts instancesDidChange and re-ran legacy
reconciliation immediately. That guard only looked for an account whose ID
equalled the instance ID, so it synthesized a second, credential-less
account alongside the real one. Signing in now marks the instance migrated,
and reconciliation treats any account on an instance as evidence its login
is already represented.
save() set hasStoredContext, so the save inside reconcileLegacyAccounts
marked the still-default .local context as explicitly chosen and the
legacy-context migration that ran immediately after did nothing. Upgrading
users with a selected server account silently lost it. Only setContext,
applySyncedContext and the migration itself record a choice now.
An Apple Account change re-resolved the credential scope but never told
AccountsManager to reload its identity-namespaced storage - the only reload
was a one-time launch task - so the previous identity's accounts, context
and outbox stayed live under the new one, as did cached per-account
subscription providers. The scope now posts when its resolved value
changes; the accounts store reloads and the provider cache is dropped.
Fourteen call sites still read the pre-revamp instance-keyed Keychain item,
which a new login no longer writes: reopening the source screen showed the
account as signed out, and feed, browse, home and import operations treated
it as unauthenticated. They now resolve the credential through the account
layer, preferring the selected context's account and otherwise any
signed-in account on that instance.
Signing in while the identity scope was still unresolved created the
account in a temporary namespace that reloadForCurrentIdentity later
discards, while credential storage silently refused the write and the UI
reported success. Login is gated on a resolved scope and verifies the
credential actually stored before reporting success, rolling back a
newly-created account if it did not.
SubscriptionService held one provider per backend and each resolved its
instance from settingsManager.subscriptionAccount, falling back to "the
first enabled Invidious instance" when that was unset. With several logins
possible on one instance that model breaks twice: two accounts would share
a single in-memory subscription cache, and the fallback could send an
account's request to a server it holds no credential for.
Providers are now built per account and cached by account ID, and each
resolves its instance and credential from the account it was built for -
no fallback. The protocol's accountType becomes a DataContext, which is
what actually identifies a provider now.
Views that branched on "is the subscription account local" read the context
instead, and the settings picker writes both stores. Keeping the legacy
setting in step is deliberate for now: several views still read it, and
letting the two diverge would show one account in the UI while serving
another's data. The Phase 3 switcher removes the last readers.
Signing out discards the account's provider so its cached subscriptions
cannot be served after the account is gone.
Yattee 2 could hold at most one login per instance: a Keychain item keyed by
the instance UUID and a Bool on the instance. This adds the account layer
the rest of the revamp builds on.
DataContext (.local or .account) replaces SubscriptionAccount as the thing
that selects a data set. The legacy value maps onto it as an identity
mapping, because migrated accounts deliberately reuse their instance UUID.
Contexts carry an app-managed changedAt, and a migrated one is stamped at
the epoch so any explicit choice on any device outranks it whatever order
they arrive in.
AccountCapabilities makes what a backend supports a single switch in
InstanceType.accountCapabilities, verified against each API surface -
Invidious has history and playlist privacy, Piped has neither. Call sites
ask for capabilities instead of switching on instance type, which is the
seam the planned per-user Yattee Server accounts land on.
AccountsManager owns accounts and the selected context, stored per identity
scope so a switch of Apple Account cannot show or upload the previous one's
list. Legacy single-logins become accounts through a repeatable
reconciliation rather than a one-shot at launch, because instances arrive
over iCloud whenever they arrive; a permanent marker set is what makes
re-running safe, since usesAccountLogin stays true on the instance forever
and would otherwise recreate a deliberately deleted account. A dirty-flag
outbox records what still needs uploading so an offline change survives a
crash. The legacy subscriptionAccount selection is migrated one way,
reading local defaults first and then the raw KVS value - the previous
release published that key to iCloud but never read it back, so on a fresh
install it exists only there.
DataContextValidator replaces SubscriptionAccountValidator, which corrected
any selection it could not immediately verify. That was safe when a context
was just "an instance I am logged into", but with credentials arriving
asynchronously it would discard the user's choice during a slow Keychain
sync. Validity is now tri-state: anything that might still resolve keeps
the selection and falls back to local for reads only, with server mutations
disabled rather than silently retargeted at local data; only an actual
deletion corrects the stored context.
EditSourceView's single log in/out pair becomes a list of accounts with
per-row status, sign-in-again and sign-out-and-remove, plus the explicit
"use saved login from this device" action - the one-tap adoption that
replaced automatic adoption after the Keychain provenance measurement. On
tvOS a credential-less account offers a local sign-in instead, since Apple
never syncs app Keychain items there and the state will not resolve on its
own.
InstanceLoginView now returns the identifier alongside the credential so
the caller can file it in the Keychain beside the secret, keeping login
identifiers out of synced metadata.
Fifth review pass, which prompted measuring whether SecItemDelete can be
conditioned on a value. It cannot: a delete issued with a deliberately
wrong kSecValueData still returns 0 and removes the item, so there is no
compare-and-delete and every check-then-delete is a race by construction.
That measurement decides both fixes here.
Adoption no longer rolls back its own write when the verification read
comes back empty. The rollback could only ever be a blind delete of
whatever occupied the key by then - including a credential synced from
another device in the gap - and it would propagate. Nothing is deleted; no
marker is written, so the account stays signed out and an ordinary sign-in
recovers it.
Legacy deletion re-reads immediately before deleting and aborts if the
value moved, shrinking the window to a single call gap and catching a
replacement that landed while the fingerprint was being computed. A
residual race remains and is documented at the call site: closing it is not
possible with these APIs, and never deleting would leave a credential the
user explicitly removed sitting in their iCloud Keychain.
The debug provenance report repeated the same nil-token ambiguity the scope
logic was fixed for, reporting "no Apple account signed in" when iCloud
Drive is merely off. It now reports the token as unavailable and shows
CloudKit's account status separately, so a re-run cannot draw a false
conclusion from it.
Yattee 2 keys credentials by instance UUID with no notion of which Apple
account they belong to, and reads them back with SynchronizableAny. Apple
documents that Keychain data may stay on device when a user signs out and
chooses to keep it, so whoever signs in next would be served the previous
user's credential - guaranteed for migrated accounts, since local instances
survive the switch and the migrated account UUID is deterministically the
instance UUID.
Credentials are now keyed "<identityScope>|<accountID>", where the scope is
the CloudKit user record (identical across a user's devices, so iCloud
Keychain sync still works). Usernames get the same scope and live beside the
credential, keeping login identifiers out of synced metadata.
The legacy unscoped item is never adopted automatically. A strict
kSecAttrSynchronizable == true query was the candidate discriminator for
"this secret belongs to the current Apple account", but measured on iPadOS
27.0 a retained item keeps the attribute and still matches that query with
no Apple account signed in at all - the flag records what was once requested,
not live iCloud Keychain membership. Reads therefore never fall back to the
legacy key; adoption is an explicit user action that copies the item,
verifies the scoped read-back, then records a marker bound to the identity
and a salted fingerprint of the secret. The legacy item is retained, because
deleting a synchronizable item propagates and would log out devices still on
the previous release mid-rollout. Deleting it later needs all three of: a
marker on this device, matching identity, and the item still holding the
adopted secret.
The instance-keyed path is untouched, so no shipping call site changes
behavior. BasicAuthCredentialsManager stays instance-keyed - proxy auth
belongs to the instance, not to a user account.
While the scope is unresolved (first launch under a new Apple account,
CloudKit unreachable) reads report "can't tell yet" via isScopeResolved
rather than "signed out", and writes are refused rather than guessing a
scope. Login state is cached per scope so one identity's cache can never
answer for another.
Also adds the debug-only harness the measurement was taken with, under
Advanced > Developer > Experiments.
Text color, border color, background color, border size, and bottom
margin were rendered as static rows on tvOS (ColorPicker and Slider are
unavailable there), so the focus engine skipped over them and they could
never be changed.
Replace them with menu pickers following the existing font size pattern:
- Text/border/background color: 9 preset colors with a swatch in the
row label; stored values snap to the nearest preset
- Background opacity: 25-100% steps (iOS parity, where ColorPicker
supports opacity for the background)
- Border size: 0.0-5.0 in 0.5 steps
- Bottom margin: 0-50% in 5% steps
Verified on tvOS simulator: rows are focusable, selections persist and
are rendered by MPV during playback.
When an Invidious/Piped account is active, several views read the local
SwiftData subscription store instead of the account, so counts
contradicted each other (Home tile said 10 while the Channels list
showed the server's 2):
- Home Channels tile now uses the provider's count via new
SubscriptionService.cachedSubscriptionCount (fetched once in the
background when the server cache isn't populated yet)
- Export footer and export content use the active account's list;
export runs async with a spinner and surfaces fetch errors as a toast
- CSV/OPML import routes through SubscriptionService.importSubscriptions,
subscribing on the server for server accounts instead of silently
writing to the invisible local store
- ChannelView subscribe-state is corrected via the provider after the
optimistic local-store read; unused *Sync write helpers removed
- Server-account subscribe/unsubscribe/import now post
subscriptionsDidChange so other views refresh
- New "Delete Local Subscription Data" section in Subscriptions
settings (visible with a server account) clears the local store and
queues CloudKit deletions so iCloud doesn't restore it
Sideloading tools re-sign the app with a team that cannot register the
iCloud.stream.yattee.app container, and CKContainer(identifier:) fatally
traps when the entitlement is missing. Detect availability by parsing
the embedded provisioning profile, skip creating any CloudKit objects
when the entitlement is absent, and show an explanation in iCloud
settings instead of the sync toggle.
After reinstalling the app and importing sources from iCloud, remote
server instances with Keychain-only credentials showed no indication
that login is needed, unlike WebDAV/SMB sources.
- Show the orange key icon for Yattee Server instances without stored
basic auth credentials (auth is always required for this type)
- Persist usesBasicAuth/usesAccountLogin flags on Instance (synced via
iCloud) so missing Invidious/Piped/PeerTube credentials are detectable
too; maintained on credential save/delete, login/logout, and legacy
migration
- Centralize the check in AppEnvironment.needsCredentials(for:) used by
SourcesListView, MediaSourcesView, and SourceRow
- Backfill the flags at launch from credentials currently in the
Keychain so existing setups publish them without a re-login
On tvOS a Form row is one focusable unit, so the legacy account/source
cards - header, credential fields, and both buttons stacked in a single
row - focused as a whole and every click fired the first button (Remove),
making credentials and Import unreachable.
- Split the tvOS rows into individually focusable rows (info header,
fields, Import, Remove), keeping the card layout on iOS/macOS
- Wrap the tvOS entry points in TVSidebarDetailContainer and drop the
tvOS navigationTitle that rendered as a ghost behind the form
- Present the review screen from the first-launch alert as a
fullScreenCover on tvOS: sheets render as a small transparent card
that cannot fit the sidebar layout (Menu button dismisses the cover)
- Dim disabled TVSettingsButtonStyle buttons
- Widen the row icon frame for tvOS glyph sizes and skip the caption
when it would repeat the host shown as the title
Hide the toggle on macOS, where apps are never suspended and playback
always continues regardless of the setting. Pause playback on iOS when
backgrounding with the setting disabled — the audio background mode
previously kept mpv playing, making the off state a no-op.
Move the continue-playing policy into PlayerService and make the
backend's background render pause unconditional (PiP-gated only):
it is a GPU optimization that should not depend on the setting.
Also ungate the player sheet visibility handlers, which manage
foreground mini player rendering unrelated to background playback.
The read-only defaults list grouped tvOS with iOS as ao=audiounit, but
MPVClient actually sets ao=avfoundation,audiounit on tvOS (avfoundation
first because audiounit can't open 32-channel HDMI Atmos routes).
Display-only fix; no playback behavior change.
On macOS 27 beta, mpv's coreaudio AO fails to initialize (channel layout
set rejected with paramErr -50). The existing ao=coreaudio,avfoundation
fallback restored audio, but exposed an mpv bug: init() registers a HAL
hotplug listener (AudioObjectAddPropertyListener on the system object,
with the raw ao pointer as user data) before the step that fails, and
the coreaudio_error path never unregisters it. mpv then frees the ao
and falls back to avfoundation, leaving a dangling listener per
playback start. The next audio device change (AirPods connect,
sleep/wake device republish) invokes the listener with the freed
pointer and crashes with a use-after-free SIGSEGV on the
HALC_ProxyNotification Call Listener Queue (observed in build 262;
faulting address was reused "texture_" shader string memory).
Gate the AO order by OS version: on macOS 27+ use
avfoundation,coreaudio so the failing coreaudio init never runs; older
macOS keeps the native coreaudio first with avfoundation as fallback.
Update the read-only defaults mirror in MPV options settings to match.
The NavigationStack-level background wasn't enough: macOS draws its own
translucent material behind pushed navigation pages, and Form/List
containers draw a translucent scroll background on top of anything
placed behind them, so several pages kept the wallpaper-tinted look.
Bake the opaque background into SettingsFormContainer (covers all pages
built on it, pushed or not) and add opaqueSettingsFormBackground(),
which hides the container's scroll background before applying the
opaque one, to every macOS-reachable Form/List settings page: player
controls and its sub-editors, sidebar settings, edit source, legacy
data import, subscription/playlist import, customize home and its
shortcut style page, and the log viewer. Contributors, translators and
the remote control device page get the plain opaque background.
Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
Adds an option in Appearance settings to use different accent colors
in light and dark mode, with a toggle to keep a single shared color
(the default, preserving existing behavior).
- New synced settings keys: accentColorDark, customAccentColorDark,
useSeparateDarkAccentColor; dark values fall back to the light
selection until explicitly set, so no migration is needed
- resolvedAccentColor returns a dynamic platform color (UIColor trait
provider / NSColor appearance provider) when the toggle is on, so
the root tint and all direct readers adapt to light/dark
automatically without consumer changes
- Accent Color section shows two stacked preset+custom grids with
Light/Dark headers when enabled, extracted into AccentColorGrid;
CustomAccentColorButton now takes bindings so the macOS color panel
edits whichever target was last opened
- New DarkAccentColorTests covering sync contract, default-off state,
fallbacks, and toggle-off resolution
Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
Add a Custom swatch to the appearance settings accent color grid,
backed by a hex value stored in settings (synced via iCloud like the
rest). iOS uses the native ColorPicker wheel; macOS uses a circular
swatch that opens NSColorPanel, since the SwiftUI color well looks out
of place among the circles. All accent consumers now read the resolved
color through SettingsManager.resolvedAccentColor.
The indigo preset is retired from the grid but kept in the enum, so
users who selected it keep their color until they pick another one.
Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
The Filter/Export/Clear buttons appeared twice on macOS: inline next
to the search bar and again inside the ellipsis toolbar menu. Show
them once as plain toolbar buttons on macOS; keep the menu on iOS/tvOS.
macOS 27 beta's AUHAL rejects mpv's channel-layout property with paramErr
(-50), so the coreaudio AO fails to initialize and playback is silent.
Change the macOS ao option from "coreaudio" to "coreaudio,avfoundation"
so mpv self-heals: older macOS keeps using coreaudio unchanged, while the
beta drops to avfoundation (AVSampleBufferAudioRenderer) instead of playing
no sound.
Replace the log-content-with-copy sheet with an NSSavePanel that writes
the exported logs to a .txt file, matching the subscriptions and preset
export flows. ShareSheet presentation is now iOS-only; tvOS export is
unchanged.
Playback menu seek items now follow the durations from the active player
controls preset instead of hardcoded 10s/30s labels. AppEnvironment caches
the active preset's center settings (refreshed on preset change
notifications) so menu commands can read them synchronously.
Add secondary seek durations (default 30s) to CenterSectionSettings,
configurable on macOS in Seek Durations settings, used by Shift+arrows in
the player and Cmd+Shift+arrows in the Playback menu.
Also let Cmd-based key equivalents pass through the player keyboard
monitor so menu shortcuts (e.g. Cmd+Option+arrow previous/next video)
work while the player window has focus, and unify macOS built-in preset
primary seek at 10s to match iOS/tvOS (built-in presets version 8).
Match the global Search view by pinning the searchable field and
trailing buttons to the right edge on macOS 26 via a flexible
ToolbarSpacer at .primaryAction. Applied to History, Bookmarks,
Manage Channels, Downloads, Instance Browse, and PeerTube Explore.
In History, the clear-history item moves from .automatic to
.primaryAction so it groups with the other trailing controls.
Buttons in the section, mini player, and pill editors can now be
removed via long-press/right-click context menu instead of only
swipe-to-delete. This also gives macOS a delete affordance in the
section editor, which previously had none.
Plain-Form sheets rendered as columns-style content floating inside
oversized fixed frames on macOS. Anchor these panels to their toolbar
buttons as popovers instead, sized to content; iOS/tvOS keep sheets.
- Search and PeerTube explore filters apply live (Cancel never
reverted anything anyway) and dismiss on outside click
- Subscriptions/Manage Channels "Subscriptions Data" opens as a sheet
from the popover's onDisappear via a pending flag, since presenting
while the popover dismisses gets swallowed
- Media browser view options drop navigation chrome on macOS
Rename the macOS "Control Bar Buttons" settings entry to "Control Bar"
and add an Auto/Light/Dark glass background picker on that page. The
choice is stored per-preset as GlobalLayoutSettings.controlBarTheme and
forces the bar's glass color scheme via glassBackground(colorScheme:).
Render the macOS control bar button row and top bar from the active
preset's sections via a new MacOSControlsSectionRenderer, replacing the
hardcoded QuickTime-style rows in MacOSControlBar. Add a keepOnTop
button type and macOS-specific button availability lists, and adapt the
player controls settings editors (preset editor, section editor, button
configuration) for macOS.
Also apply the preset font style to the control bar time labels, and
update the built-in macOS Default preset order (queue before transport,
fullscreen at the end), bumping builtInPresetsVersion to 7.
Replace the three-way macOS Player Mode picker (Separate Window /
Floating Window / Inline) with a single 'Play in a separate window'
toggle in Playback settings, and move the always-on-top choice to a
pin button in the player's top bar (window mode only).
Floating was really a transient window property (NSWindow.level), not
a peer of the window/inline structural choice — so it belongs on a live
control, not in Settings. The pin state persists across sessions.
- Replace MacPlayerMode enum with macPlayerSeparateWindow /
macPlayerFloating bool settings
- Branch runtime presentation and window level off the two bools
- Add pin.fill/pin toggle to MacOSPlayerControlsView top bar
- Update localization; drop obsolete playerMode strings and enum tests
Sheet dismiss buttons rendered as an iOS-style circular glass "X" on
macOS 26, which looks foreign on the desktop. Add a shared
sheetCloseToolbarItem helper that renders a native text "Close" button
on macOS while keeping the compact icon-only xmark on iOS/tvOS, and
replace the ~14 duplicated inline close buttons across sheets with it.
Drop the dashEnabled toggle and its plumbing. Stream auto-selection now
keeps DASH as a candidate but ranks it strictly below progressive and
HLS formats, so DASH is only chosen when it is the only format available
(including live-stream fallback after HLS). The manual quality selector
hides DASH entirely on iOS/tvOS/macOS. Reconciles BackendSwitcher, which
previously treated HLS/DASH equally, to use the same DASH-last ranking.
The Test Connection buttons only probed API/server reachability, which
was misleading: a green result said nothing about whether videos would
actually play. Remove both the remote-server and WebDAV bandwidth test
buttons and all code exclusive to them (testBandwidth, BandwidthTestResult,
and orphaned localization keys). Add-time connectivity validation for
SMB/WebDAV sources is retained.
Legacy v1 instances without an account had no import path after the
switch to explicit account review. Surface them in a separate "Sources"
section of the legacy import view where they can be added with a single
tap (no sign-in) or removed, mirroring the accounts flow. Instances tied
to an account stay in the Accounts section and are not duplicated.
Detection now gates the entry points and first-launch prompt on any
legacy data (accounts or sources), not accounts alone.
The manual account-import flow made the old silent instance-import
pipeline unreachable. Drop the orphaned parsing, reachability checks,
batch import, result/error types, and the old import row view, plus the
httpClient dependency they were the only consumer of. Also remove the
LegacyAccount.password field, which was parsed but never read.
Pre-macOS 26 doesn't reliably surface a detail-pane NavigationStack's
toolbar confirmation items in the window title bar, leaving the
"Add Source" actions invisible. Gate the toolbar placement behind
macOS 26+ and fall back to an inline form/header button on older
macOS across the add-source views and the sources list.
Surface audio-delay (±10/±100 ms) and video-sync-mode controls behind an
"A/V Sync" navigation row in Advanced settings rather than inline, keeping the
Advanced page uncluttered. Audio delay applies live to the running MPV
instance; sync mode takes effect on next playback.
Lets the Apple TV switch its HDMI output to match the playing video's
frame rate and dynamic range via AVDisplayManager.preferredDisplayCriteria,
driven from MPV's container-fps and video-params/gamma. Two opt-in toggles
(default off) live under Playback → Display on tvOS; both are no-ops on
other platforms. Anchor an AVKit class symbol so the linker keeps AVKit
linked — Swift only autolinks AVFoundation here, and without AVKit the
UIWindow.avDisplayManager category isn't loaded at runtime.
Wrap pushed view in TVSidebarDetailContainer, list custom options
first so focus engages on a row, mark default options focusable so the
list scrolls. Replace toolbar-based Add/Edit sheets with padded VStacks
and inline confirm buttons. Cache customMPVOptions in @Observable
backing storage so writes refresh the list immediately.
Surfaces the existing iOS/macOS Background Playback setting in the tvOS
Playback settings, defaulting to off so audio stops when the user leaves
the app via the TV button. Pauses playback on .background/.inactive when
the toggle is off, regardless of audio route — the user's setting wins
over AirPlay/HomePod handoff. Also auto-shows the tvOS player controls
when returning to foreground so the paused state is immediately visible
and actionable.
tvOS's sidebarAdaptable TabView leaves the previously-pushed detail view
visible after the user picks another sidebar item, until they manually
press Menu. Broadcast a notification on tab change so any pushed
TVSidebarDetailContainer dismisses itself, and reset each tab's
NavigationPath. Also drop a redundant inner NavigationStack in the tvOS
SettingsView so subpages register on the tab's outer stack.
Lets the auto stream selector pick formats whose codec isn't hardware
decoded on the current device. Defaults off; when on, 4K VP9/AV1 can be
auto-selected on Apple TV models without those decoders. Software-decoded
streams also move into the Recommended section so the selection stays
visible without enabling advanced stream details.
Settings → Notifications → Manage Channels: wrap the tvOS NavigationLink
destination in TVSidebarDetailContainer(showsDismissButton: true) so the
no-subscriptions, error, and loading states all have a focusable Done.
Channels sidebar tab: lift the tvOS search field + View Options button
out of the loaded-channels branch and render it above every state. The
empty state previously had zero focusable elements, leaving the right
pane blank when swiping in from the sidebar.
TVSidebarDetailContainer now exposes a showsDismissButton flag instead of
always attaching a Done toolbar item. The button is only enabled where a
view can end up with no focusable element on its own — Device
Capabilities (informational rows) and the Import Playlists/Subscriptions
flows.
Wrap Contributors, Translators, Acknowledgements, and Device Capabilities
destinations in TVSidebarDetailContainer for the consistent sidebar look,
and make the Translators/Acknowledgements rows focusable on tvOS by
wrapping them in Buttons so the Menu remote button can pop the stack.