Commit Graph

622 Commits

Author SHA1 Message Date
Arkadiusz Fal
de8ed91029 Use a scheme-adaptive background for tvOS full screen covers
tvOS full screen covers are transparent, so each one filled itself with
a hardcoded black — invisible black-on-black text under the light color
scheme (e.g. the View All Comments page). A shared
tvFullScreenCoverBackground() modifier now picks black or light gray
from the color scheme; applied to the comments cover, subscription
destination picker, instance login, edit source, iCloud progress, and
legacy accounts import covers.
2026-09-19 23:04:01 +02:00
Arkadiusz Fal
40b906167c Fix white text in video info view under the light tvOS color scheme
The video title and TVScrollableDescription hardcoded white text, which
was unreadable on light backgrounds. Description colors now adapt to the
color scheme by default; the player details panel opts back into white
via overlaysDarkBackground since it always sits over a dark backdrop.
2026-09-18 23:59:57 +02:00
Arkadiusz Fal
629561f057 Keep the system accent color on tvOS
Views reading resolvedAccentColor directly (comment links, home
shortcuts, bookmarks, player panels) picked up accent colors synced
from iOS via iCloud. tvOS now always resolves to the system default.
2026-09-18 23:59:54 +02:00
Arkadiusz Fal
17aa723cd0 Prefer the active instance when selecting a playback instance 2026-09-18 23:59:49 +02:00
Arkadiusz Fal
5cd8d586c6 Add build date to the About settings view 2026-09-18 23:59:29 +02:00
Arkadiusz Fal
d22abf6088 Add an option to share videos via privacy frontend URLs
When enabled (the default), share links to YouTube videos point to the
active source's web watch page instead of youtube.com. Invidious serves
its frontend at the API URL; Piped uses its configured frontend URL.
Videos the active source can't serve (PeerTube, extracted sources) and
sources without a web frontend keep their original links.

The toggle lives in Settings > Layout & Navigation > Sharing on iOS and
macOS, and is shown only when an enabled source has a web watch page.
2026-09-18 23:57:34 +02:00
Arkadiusz Fal
172d712f25 Add an optional frontend URL for Piped instances used by browser actions 2026-09-18 23:36:11 +02:00
Arkadiusz Fal
045e0ba574 Add refresh, accounts, and link actions to the instance browse menu
The three-dots menu now offers Refresh, an accounts entry (sign in when
nobody is, otherwise a submenu that switches the data context, adds a
login, or signs out the active account with the shared removal
confirmation), and Copy URL / Open in Browser on iOS and macOS.
Login and sign-out reuse InstanceLoginView, AccountSignInHandler, and
the same confirmation strings as the edit sheet's accounts section.
2026-09-17 23:59:37 +02:00
Arkadiusz Fal
d742fec41a Replace the instance browse view options button with a three-dots menu
The toolbar (and tvOS inline) button now opens a menu with Edit and
Remove actions for the browsed instance above the existing View Options
entry. Edit opens the shared EditSourceView; Remove confirms with the
same account-deletion disclosure as Settings and pops the view.
2026-09-17 23:59:32 +02:00
Arkadiusz Fal
2a816bd893 Move basic auth credentials into the Security section in the instance edit form 2026-09-17 23:59:28 +02:00
Arkadiusz Fal
93886b20ea Surface video detail load failures in the error view for placeholder videos
Opening a members-only video from a history or feed row got a 500 from
the videos API and left the detail view blank: the retry error view only
covered the videoID init mode, while the details fetch for a loaded
video failed silently.

Record the details-fetch error and route it into the same error view
when the displayed video is a bare placeholder (no author, no
thumbnails), show a spinner while that fetch is in flight, and make
retry re-run both loaders. Videos with real partial metadata keep
rendering as before.
2026-09-17 23:34:15 +02:00
Arkadiusz Fal
101880d35f Route instance subscription imports through SubscriptionService with a destination picker
ImportSubscriptionsView wrote every imported channel straight into the
local SwiftData store via dataManager.subscribe(to:), regardless of the
selected account context. Imports now go through
SubscriptionService.subscribe(to:in:) aimed at a destination the user
picks — the local store or any logged-in account that supports
subscriptions, minus the source instance's own account, the same roster
the subscribe menus offer.

The already-subscribed marks are asked per destination through a new
SubscriptionService.subscribedChannelIDs(in:) helper (cold server
destinations fetch their list once, generation-fenced like the other
reads) and reload when the destination changes. Individual adds show a
per-row spinner while the server write is in flight, alreadySubscribed
counts as success, and Add All runs serially with an added/failed tally.

Also drop the "Invidious" wording from the import empty state — the
view serves Piped sources too.
2026-09-17 23:23:56 +02:00
Arkadiusz Fal
3e623cba2f Speak title, channel, and duration as the video row VoiceOver label
The row's accessibilityLabel was the debug string video.row.<id>, so
VoiceOver read a raw YouTube ID for every row. Build the label from the
DeArrow-aware display title, author name, and duration instead; the
accessibilityIdentifier stays unchanged for UI tests.
2026-09-17 19:18:41 +02:00
Arkadiusz Fal
fe0a5e725c Fence stale-generation writes in Yattee Server feed error and progress paths
A stateless-server feed request that failed after an account switch could
write the previous account's error state over the new account's feed, and
a stale poller could flash the old context's partiallyLoaded counts. Guard
both writes on the captured generation, matching the existing completion
fence.
2026-09-16 23:37:00 +02:00
Arkadiusz Fal
8677ac39d3 Prune stale keys from Localizable.xcstrings 2026-09-16 23:23:33 +02:00
Arkadiusz Fal
b413648ab9 Hold the Accounts settings entry until a source can hold an account 2026-09-16 22:45:41 +02:00
Arkadiusz Fal
f6fde48767 Rebuild the sidebar switcher menu when the context name changes 2026-09-16 21:14:22 +02:00
Arkadiusz Fal
59c4f27bc7 Slim the iPad sidebar switcher pill to icon and account name 2026-09-16 18:27:17 +02:00
Arkadiusz Fal
5d33e4a558 Hide toolbar account switchers where the sidebar already shows one 2026-09-16 18:16:10 +02:00
Arkadiusz Fal
a5ed91620f Back the iPad sidebar switcher with its own pill 2026-09-15 23:57:59 +02:00
Arkadiusz Fal
0aa79221f4 Pin the account switcher under the iPad sidebar like macOS 2026-09-15 23:56:46 +02:00
Arkadiusz Fal
e28ca98831 Give the tvOS New Playlist row air between icon and title
A tvOS Label sets its plus glyph nearly against the text at this size;
the row spells the gap out with an HStack there, shared by the local and
remote sections. Other platforms keep the plain Label.
2026-09-15 23:23:47 +02:00
Arkadiusz Fal
ba553162f4 Name the tvOS destination picker in its row
A tvOS menu picker shows only its value, so the collapsed control read
as a bare account name. The menu's label is now the whole labelled row -
'Account' leading, the selection trailing, the same shape as the
settings switcher row - with the choices as a checkmarked picker inside
the menu. Other platforms keep the plain menu picker, which already
carries its label.
2026-09-15 22:56:10 +02:00
Arkadiusz Fal
62f7e3eeb3 Let the playlist selector's account picker collapse on tvOS
The tvOS branch pinned the picker to .inline over a fear that a menu
would dismiss the sheet it sits in — that is true of the hardware Menu
button and a fullScreenCover, not of a menu-style picker in a sheet. The
inline rows meanwhile filled the card with every account before the
first playlist. One .menu picker on every platform, device-verified:
picking another account swaps the section in place.
2026-09-15 18:03:04 +02:00
Arkadiusz Fal
753cda9661 Label the local context by where its data actually lives
"Yattee (iCloud)" was hardcoded for the local data context even with
iCloud sync disabled, when the data is purely on-device. The name now
comes from DataContextValidator.localContextName, which checks
iCloudSyncEnabled and falls back to "Yattee (This Device)".
2026-09-15 09:02:18 +02:00
Arkadiusz Fal
50e32dbe5a Make every tvOS switcher a menu and retire the full-screen picker
The header button and the settings row both raised a fullScreenCover to
pick a context; a Menu offers the same rows in place for one click,
matching the pickers elsewhere in Settings. With every presentation now
a menu, the picker view and the rows' inMenu flag had nothing left to
serve. The subscriptions header also cedes its width to the account
name: the view-options button keeps only its icon, since two full
titles wrapped into tall two-line lozenges in the tvOS sidebar.
2026-09-14 18:59:00 +02:00
Arkadiusz Fal
ded53d7962 Fold the tvOS account actions into a press-raised dialog
Each account was four focusable rows tall on tvOS — Use this account,
Sign in again, Rename and Sign out all spelled out as siblings, a lot
of clicking past on a remote. The row itself is now the one focusable
unit: pressing it raises the same actions as a confirmation dialog,
mirroring the context menu the other platforms already attach.
2026-09-14 18:28:28 +02:00
Arkadiusz Fal
b917049243 Keep a fenced feed refresh from stranding the loading flag
The refresh captured its account generation before hopping onto the
detached task it runs in. That hop is a suspension point: a context
handler running in the gap - the wake-time status flap bumps the
generation there - left the request carrying a stale generation. Its
completion was then fenced out, and the fenced return deliberately
leaves isLoading alone, so nothing current owned the flag: it stayed
true, every later refresh skipped itself as already loading, and the
retry paths only watch for errors - the feed sat on a spinner nothing
would ever finish.

The generation and context are now read at dispatch time, so the
request that claims isLoading always carries the generation current at
that moment and its completion is the one that releases it. The
stateless-server poller's terminal writes get the same fence: a stale
poller must not hand a newer request's spinner an early ready.
2026-09-14 09:02:32 +02:00
Arkadiusz Fal
a28346148a Show the tvOS channels sidebar for a single-channel account
The sidebar hid itself below two channels, so an account with one
subscription lost the channel list, the manage-channels entry and the
switcher's persistent home next to View Options - and switching from a
many-channel account read as the sidebar breaking rather than a design
choice. One channel is enough to earn the sidebar; only an empty account
goes without it.
2026-09-14 09:01:26 +02:00
Arkadiusz Fal
82ef5267c4 Let the sidebar's channels and playlists follow the data context 2026-09-14 07:11:28 +02:00
Arkadiusz Fal
f2c4721e5a Leave the macOS toolbars to the sidebar's account switcher 2026-09-13 20:38:26 +02:00
Arkadiusz Fal
69d22cdc98 Withhold every account switcher until an account exists to offer 2026-09-13 20:16:14 +02:00
Arkadiusz Fal
5136b79f49 Let the user hide the account pickers outside Settings 2026-09-13 18:56:18 +02:00
Arkadiusz Fal
ce97aa1031 Let the tapped playlist row spin while its add is on the wire
The in-flight gate now remembers which row it closed for, and that row
trades its plus for a small spinner until the checkmark - or the error
toast - takes over. Local adds are instant and keep going straight to
the check.
2026-09-13 14:01:06 +02:00
Arkadiusz Fal
80ed81b0e4 Let the playlist selector accept only one add per presentation
The tapped row stays on screen for the half second the checkmark shows -
and for a remote add, the whole round-trip - so quick repeated taps each
started another add and the video landed in the playlist several times.
One gate covers both paths; a failed remote add reopens it, since
failing is the one way the sheet stays up to retry from.
2026-09-13 13:48:25 +02:00
Arkadiusz Fal
a742aee353 Let the playlist selector show one destination at a time
A picker at the top - seeded from the selected data context, private to
the sheet from then on - replaces the stacked per-account sections, so
picking where a video goes no longer scrolls past every login and never
switches what the app is browsing as.
2026-09-13 08:07:06 +02:00
Arkadiusz Fal
b8921387dd Let the accounts screen own the switcher instead of the settings list
The main settings list carried both an Accounts row and a switcher row
beside it — two account controls at the top level, one of them opening
the screen the other belonged on. The switcher row moves to the top of
the accounts screen as a Current Account section, where the selection
it names sits directly above the checkmarked row it corresponds to.

The subscriptions-settings row and the macOS sidebar footer keep
theirs: those are contextual surfaces, not the settings list.
2026-09-12 23:58:32 +02:00
Arkadiusz Fal
b08185652e Let the switcher refuse the states only the user can fix
Picking a sign-in-required account landed on the local fallback with a
banner — the switch achieved nothing, and the fix lives in Settings.
isSwitchable now refuses a rejected or missing credential the same way
it already refused a switched-off source: the row stays listed with its
status in the title, disabled.

The line runs between states the user must act on and states that heal
on their own. Unreachable stays switchable — while the device itself is
offline every account reads unreachable, and refusing those would lock
the whole switcher — and so do checking and instance-pending, which are
refused nothing they may turn out to deserve.

Reaches every presentation through the shared rows, and withholds
"Use This Account" on the accounts screen through availableContexts.
The tests sign their usable accounts in now that a fresh account under
a resolved scope defaults to credentialMissing and is refused.
2026-09-12 23:55:07 +02:00
Arkadiusz Fal
a04b44e207 Let Home carry the account switcher and follow the selected account
The Home tab showed the same device-local numbers whatever data context
was selected, with no way to switch from there. Now it carries the same
DataContextSwitcherMenu as Subscriptions, History and Playlists, and its
data follows the switch:

- The history card and recent-history section resolve by the same
  HistoryDisplayMode rule as the history screen: an account with
  server-side history gets one probe of it (count up to 500, shown as a
  floor past that) and hydrated leading rows; a failed or still-out
  fetch reads as an em dash, never as the device's numbers under an
  account's name. Local writes no longer refetch the server; a posted
  watch does.
- The playlists card reads the remote playlists cache live in an
  account context, kicking the first fetch when the playlists screen
  never did.
- Context, credential and status changes reload the counts and refill
  the feed section the cache clearing just emptied; the channels-count
  fetch drops its result if the context moved while it was out.

Bookmarks, continue watching and downloads stay device-local on
purpose: they have no server counterpart. The notification
subscriptions moved into a dataObservers helper to keep the body within
the type-checker's budget.
2026-09-12 23:50:15 +02:00
Arkadiusz Fal
839de238d3 Let account labels call the source by its title, not its host
An account with no user-set label was named username@host everywhere it
appears — the switcher menu, the settings rows, the playlist selector —
which overflows a menu row the moment the host has a subdomain. The
default name and the switcher's disambiguation suffixes now use the
instance's title when one is set, falling back to the host as before.
2026-09-12 23:44:35 +02:00
Arkadiusz Fal
ea66d8f303 Let the signed-out feed placeholder say something
subscriptions.notAuthenticated.title/.description had no English values, so the Subscriptions ContentUnavailableView rendered raw with empty text when the selected account lacks authentication. Fill them in wording that matches the account status badges ("Sign in required" / "Sign in again").
2026-09-12 19:36:39 +02:00
Arkadiusz Fal
03037e6769 Let a refused host count as unreachable and the switcher own every login
Residual round after the macOS fix verification, five closures:

A connection refusal or a failed DNS lookup arrived as APIError.unknown,
which no classifier reads — so an account pointed at a downed instance
never went unreachable, never fell back to local data, never armed a
retry. New APIError.connectionFailed carries the system's own sentence
and lands as unreachable/offline in AccountsManager, InstancesManager
and the credential probe; a refused host now classifies in seconds
instead of never.

Subscriptions' periodic retry armed only for networkError, while the
local fallback's yatteeServerRequired — shown exactly when the account's
server could not be reached — waited on a status expiry nothing was
watching. FeedLoadState.warrantsPeriodicRetry gates on the context's
unavailability, so that screen heals on its own too, and an account that
genuinely has no Yattee Server still never loops.

The switcher dropped every context of a switched-off source, leaving the
toolbar badge pointing at a menu its own selection had vanished from.
listedContexts keeps the rows with their status in the title ("— Source
switched off"), isSwitchable refuses the pick, and the current context
keeps its checkmark.

The sidebar footer's switcher row overflowed the 140 pt column minimum:
the width floor now sits on the menu itself and ViewThatFits drops the
caption — the part the icon already says — before the account name loses
characters.

The channel menu exposed a raw SF Symbol name as its accessibility
title; the label is now a real Label rendered iconOnly, so the AX bridge
hears "Subscription Options".
2026-09-12 17:10:58 +02:00
Arkadiusz Fal
40d6f4e71f Let a status flap fence the feed instead of throwing it away
MAC3-R1 from the fix verification: refreshCachesForEffectiveContext purged
on every effective-context move, so the cache the Subscriptions fix meant
to keep was already empty when its keep-path ran. A move under an unchanged
selection now fences the feed cache non-destructively; only a genuine
selection move still purges. Also settles the destination loader's
observer callback onto the main actor without a capture-of-var warning.
2026-09-11 21:14:53 +02:00
Arkadiusz Fal
c0d38834a6 Let rows update their counts, drop fake dates, and own failed lookups
MAC6: a snapshot mutation carries its video count back to the list cache.
MAC8: playlist rows and cards are Buttons with the context menu attached,
so macOS accessibility can reach Edit and Delete.
MAC10: a published date stamped with the request's own time is dropped at
both API sentinels, and formattedPublishedDate refuses zero-interval output.
MAC11: a failed history hydration shows Details unavailable with a Try
Again action instead of keeping the raw video ID forever; an empty 200
body is a legible failure.
2026-09-11 20:28:35 +02:00
Arkadiusz Fal
3281840a8e Let the login sheet respect its edges and the account footer tell the truth
MAC2/MAC15: the macOS login form is grouped like its Edit Source parent, so
labels sit inside the sheet and the add-flow step stops clipping; footers
wrap instead of truncating.
MAC12: the account section footer consults account statuses before claiming
the user is logged in.
2026-09-11 19:36:50 +02:00
Arkadiusz Fal
94b2dff75b Let Subscriptions settle on an answer and heal when the server returns
MAC3: an error the context banner answers now reaches a terminal Feed
Unavailable view instead of the spinner branch; an effective-context move
keeps the cached feed instead of purging it; the view retries on appear,
on activation, on a status lift, and every minute while a network error
is on screen.
2026-09-11 19:08:25 +02:00
Arkadiusz Fal
b0b95195b6 Let Subscribe On know its memberships and refuse a doomed destination
MAC5: membership lives in an observable loader started from ChannelView's
task, outside the NSMenu content SwiftUI never runs lifecycle for; rows are
Toggles so the mark renders on macOS, and writes toast Subscribed or
Unsubscribed with the destination name.
MAC13: a destination whose context fails validation is disabled, the same
gate the plain Subscribe button uses.
2026-09-11 08:20:24 +02:00
Arkadiusz Fal
e661635079 Let every menu mark the current context and a disabled source admit it
MAC1: switcher rows are Toggles on macOS so the checkmark really renders,
and the settings row names its context via LabeledContent with a tooltip.
MAC4: accessible names are words, never raw SF Symbol identifiers.
MAC14: new displayStatus(for:) surfaces instanceDisabled on account rows,
badges, and the switcher tooltip without disturbing status(for:) consumers.
MAC16: disambiguated labels guard the username branch and fall back to the
source name only when rows would still collide.
Also carries the new Localizable strings for the whole macOS fix batch.
2026-09-11 08:02:26 +02:00
Arkadiusz Fal
cf07236cf7 Let a private playlist wait for its account instead of asking in public
When the credential identity scope drops to unresolved — which it
does on its own on every activation without a ubiquity token — the
account list is briefly empty, and the detail view answered "nobody
can be named" as "nobody owns it": it refetched through the public
endpoint, which replies 404 "Playlist does not exist." for a private
playlist that was on screen a moment ago (TV9). An account-loaded
playlist now waits out the unresolved window, and the reload guard
remembers that an account once read it here, so the notification that
credentials are readable again recovers the view instead of being
dropped for lack of anything left on screen.
2026-09-10 23:55:05 +02:00
Arkadiusz Fal
9562510c67 Let a failed detect offer a retry instead of a dead Add Source
After detection failed, Add Source stayed enabled and tapping it just
re-ran detection into the same error — an enabled-looking button that
visibly did nothing, forever (TV8). The error state now shows the
button that can help, Retry Detection, holds a perceptible detecting
beat even when the host fails instantly, and hides the display-name
section that made the form read as submittable. Add Source itself now
requires a detected type, so it can no longer render enabled without
one.
2026-09-10 23:50:24 +02:00