The backgroundStyle.ignoresSafeArea().overlay(ScrollView) pattern
clips the tvOS focus effect at the overlay boundary. On tvOS, render
the ScrollView directly so focus highlights can extend naturally.
Reorganize the tvOS search UI into a single header row with search
field, type filter menu, combined filters menu (sort/date/duration
with reset), and view options button. Removes the separate filter
strip between search and results on tvOS.
Form inside a sheet causes clipped rows and invisible text on focused
items due to white-on-white rendering. Use a plain List on tvOS which
handles the focus styling correctly.
The .pickerStyle(.menu) hid labels. Instead, wrap the Form in a
NavigationStack on tvOS so the default navigation picker style works
and shows both labels and values.
Dividers inside rows conflict with the tvOS focus highlight effect.
Remove dividers and the inset card background/clipShape on tvOS so
the focus effect renders cleanly without visual artifacts.
Unifies the filter strip on tvOS so all filters (sort, date, duration,
content type) use the same inline Menu style instead of mixing menus
with a segmented picker.
The filters sheet is too small and awkward on tvOS. Replace the filter
button with inline Menu pickers for Sort By, Upload Date, and Duration
directly in the filter strip. Applied to both SearchView and
InstanceBrowseView.
Use inline TextField with focusSection instead of .searchable() on tvOS
to prevent keyboard/navigation title overlap. Remove clipShape on recent
search items so tvOS focus effect is not cut off.
- Replace sheets with navigationDestination for Add/Edit Source on tvOS
(tvOS sheets have fixed size that doesn't fit the content)
- Fix focused cell clipping by replacing TVSettingsContainer's frame-based
layout with safeAreaInset, matching the main settings view pattern
- Use standard List with .listStyle(.grouped) for Sources on tvOS
- Add sidebar icons and titles to TVSettingsContainer for all settings
subviews, utilizing the left column space
- Remove redundant large navigation titles on tvOS (shown in sidebar)
- Move Edit Source Save button from toolbar into form above Delete button
for better tvOS focus navigation
parseAudioInfo() returned isOriginal=false for all streams when the
audioTrack object was present in the API response, preventing xtags
parsing from correctly identifying original tracks. This caused the
player to fall through to codec/bitrate sorting, often picking a
locale dub (e.g. Polish) instead of the original English audio.
Now determines isOriginal from both the audioTrack displayName
("original" keyword) and URL xtags (acont=original) for robustness.
Also adds isDefault to InvidiousAudioTrack for future use.
Exercises the read path end-to-end against an Invidious instance fronted
by an HTTP Basic Auth reverse proxy: adds the instance via the new
basic-auth-aware add helper, navigates to Search, runs a query, taps the
first result, waits for the player to expand and start playback, then
closes the player. Confirms that ContentService's per-instance HTTPClient
(with the basic-auth Authorization header baked in via setDefaultHeaders)
is wired correctly through search, video metadata fetch, and stream
loading.
Skips cleanly when INVIDIOUS_BASIC_AUTH_USERNAME / _PASSWORD are not set.
Three end-to-end specs that exercise the new basic-auth flows against a
real Invidious instance fronted by an nginx reverse proxy:
1. add flow: types the URL, hits Detect, fills the basic-auth fields
when the basicAuthRequired UI state appears, taps Retry Detection,
and confirms the instance lands in the Sources list.
2. state assertion: types a URL, taps Detect, and verifies the form
transitions into the basicAuthRequired state (Retry Detection button
present, no detected type) when no credentials were supplied.
3. proxied login: ensures the instance exists, then drives the standard
Invidious login flow with the proxied account credentials. Confirms
the SID Cookie auth coexists with the per-client Authorization
header on the basic-auth-aware HTTPClient.
Test infrastructure additions:
- spec/ui/support/config.rb: env-driven accessors for the basic-auth URL
and proxied-account credentials. No secrets committed.
- spec/ui/support/instance_setup.rb: helpers
add_invidious_with_basic_auth, remove_and_add_invidious_with_basic_auth,
find_basic_auth_text_fields (mirroring find_auth_text_fields), and
fill_field for tapping a discovered field by frame and typing into it.
All three specs skip cleanly when the relevant env vars are not set.
- InstanceDetector: a single 401 from one probe was over-eagerly concluded
as "credentials invalid" / "credentials required". On instances behind a
reverse proxy where one probe path (e.g. Yattee Server's /info) hits a
same-origin redirect, iOS URLSession strips the Authorization header on
the redirect and the request 401s even with valid credentials. Track 401s
across all probes and only conclude basicAuthRequired/basicAuthInvalid
when no probe matched and at least one returned 401.
- InstanceLoginView: the Invidious/Piped login flow constructed an API
client backed by the shared appEnvironment.httpClient, which has no
per-instance basic-auth headers. For instances behind a reverse proxy,
the login POST 401d before reaching the upstream login endpoint. Build a
per-instance HTTPClient with the basic-auth Authorization header baked in
via setDefaultHeaders, mirroring ContentService.httpClientWithBasicAuth.
- InvidiousAPI.login: the login function constructs its own URLSession (to
capture Set-Cookie via a redirect-blocking delegate), so it never
inherits headers from the injected httpClient. Add an optional
extraHeaders parameter and have InstanceLoginView pass the basic-auth
header through when present. PipedAPI.login uses httpClient.fetch and
inherits defaultHeaders correctly, so no change is needed there.
EditSourceView now exposes the basic-auth username/password fields for every
instance type (Invidious, Piped, PeerTube, Yattee Server), keeping the
existing required-credentials UI for Yattee Server and adding an optional
section for the others. Credentials are loaded and persisted via
BasicAuthCredentialsManager regardless of type, and clearing both fields
deletes stored credentials for non-Yattee types.
AddRemoteServerView gains a new basicAuthRequired UI state: when instance
detection hits a 401 (the entire instance is behind a reverse proxy), the
view reveals username/password fields and a Retry Detection button. The
retry calls the detector with the credentials injected as an Authorization
header; on success the form transitions into the normal detected state with
the credentials pre-populated. A repeat 401 shows an inline "invalid
credentials" message instead of restarting the flow. For non-Yattee types,
any credentials entered during the flow are persisted alongside the new
instance.
When an instance sits behind a reverse proxy that requires HTTP Basic Auth,
every detection probe (/info, /api/v1/config, /api/v1/stats, /healthcheck,
/config) returns 401 before reaching the real backend, so the type cannot be
identified. Re-throw APIError.unauthorized from each probe instead of
swallowing it, and have detectWithResult convert the first 401 it sees into
DetectionError.basicAuthRequired. Add a basicAuthHeader parameter so the
caller can retry detection after the user provides credentials; if a retry
also returns 401, surface basicAuthInvalid instead.
Replace the YatteeServerAPI setAuthHeader/buildHeaders pattern (which was
race-prone on the shared actor across multiple instances) with a generic
mechanism: HTTPClient now supports a defaultHeaders dictionary applied to
every request, and ContentService builds a per-instance HTTPClient with the
basic-auth Authorization header baked in whenever credentials are configured.
The same code path now works uniformly for Invidious, Piped, PeerTube, and
Yattee Server, so any instance sitting behind a reverse proxy that requires
HTTP Basic Auth can be authenticated regardless of backend type. Cached
default API actors are still reused when no basic-auth header is needed.
Piped accepts the session token via either an Authorization header or an
authToken query parameter (the /feed endpoint already uses the latter form).
Switch all token-bearing Piped endpoints to the query-parameter form so the
Authorization header is free for HTTP Basic Auth from a fronting reverse
proxy. Affects subscriptions, subscribe, unsubscribe, userPlaylists, and
userPlaylist (including its nextpage pagination loop).
Renames YatteeServerCredentialsManager → BasicAuthCredentialsManager so the
same Keychain-backed username/password storage can be reused for any instance
type that sits behind a reverse proxy requiring HTTP Basic Auth. Adds a
one-time migration that moves existing items from the legacy
'com.yattee.yatteeserver' Keychain service to 'com.yattee.basicauth',
preserving the iCloud-sync attribute. No behavior change for end users.
Videos that haven't premiered yet were triggering repeated notifications
on every background refresh cycle. Filter them out by checking isUpcoming
flag and rejecting videos with future publish dates.
Also decode isUpcoming/premiereTimestamp from Yattee Server feed responses
instead of hardcoding false/nil.
Extract shared TimeInterval.formattedAsTimestamp replacing 8 identical
formatTime/formattedTime implementations across player views. Remove
unused currentTime parameter from GestureSeekPreviewView. Consolidate
duplicated geometry math in MacOSControlBar into seekPreviewPosition().
Extract chapter title from inside the storyboard preview into a
standalone ChapterCapsuleView with its own glass capsule background.
The capsule follows the seek position horizontally but independently
clamps to screen edges using alignmentGuide, allowing it to be wider
than the storyboard thumbnail without going offscreen.
Display a floating time pill above the seek bar during dragging
(iOS) and dragging/hovering (macOS) when video has no storyboard
thumbnails. Includes chapter name when available.
- Fix BGTaskScheduler assertion crash on Mac Catalyst by guarding all
iOS background task APIs with isMacCatalystApp check
- Fix iPad popover crash in UIPopoverPresentationController by adding
.presentationCompactAdaptation(.sheet) to all 27 confirmationDialogs
- Fix SwiftData assertion crash when accessing deleted Bookmark model
properties during SwiftUI hit testing in BookmarkRowView
- Fix UICollectionView invalid item count crash on queue swipe-to-delete
by using ID-based removal with withAnimation instead of stale index
- Fix Range crash in storyboard download when storyboardCount is zero
Preserve user:pass credentials in instance URLs so Invidious instances
behind nginx reverse proxies with HTTP basic auth work correctly (#926).
Add displayURL property to mask credentials in the UI.
These settings don't work well on Apple TV, so exclude the
ThemeSection, AccentColorSection, and the .preferredColorScheme/.tint
modifiers from tvOS builds.
On tvOS, ContentUnavailableView inside a Group doesn't expand to fill
available space — it sizes to content and aligns top-leading. Add
.frame(maxWidth: .infinity, maxHeight: .infinity) to all instances
so they center correctly in their parent containers.
Post watchHistoryDidChange notification when a new watch entry is inserted
during local playback progress updates (but not on every progress tick).
Reload Home data when switching back to the Home tab and when the Customize
Home sheet is dismissed.
Remove 32 non-dotted keys (16 unused format specifiers, 16 word keys)
and replace with properly namespaced dotted keys following the existing
convention (common.*, player.*, search.*).
Display the view options button, channel menu, and content type tabs
immediately when the cached header is shown, instead of waiting for
the full channel data to load. The spinner now appears only in the
content area below the tabs.