Commit Graph

34 Commits

Author SHA1 Message Date
Arkadiusz Fal
5ac6b7188d Ask what a backend supports instead of what it is 2026-09-07 18:48:50 +02:00
Arkadiusz Fal
3dc129dcfa Let the banner speak only for what it answers
Standing the feed error down under the context banner was aimed at one
sentence and hit every one. The fallback to local reads asks for a Yattee
Server nobody configured, and that question the banner already answers —
but a local refresh that failed on the way out answers nothing, and it was
being swallowed too. With the banner up, an offline refresh left the last
feed on screen with no explanation and no retry: the state said error, the
view said nothing.

The decision now names the errors the banner covers. `yatteeServerRequired`
and `notAuthenticated` are the account the banner is already talking about
and stand down; a `networkError` is news the banner does not carry and
keeps its own message and its retry button. It lives on `FeedLoadState` as
`displayedError(contextBannerVisible:)` so the rule is one function rather
than a guard in a view property, and so it can be tested.
2026-09-05 23:49:10 +02:00
Arkadiusz Fal
6bf3942529 Send a creation where the button said it would go
Creating a playlist while an Invidious account was the selected context
wrote it to the device. No request left the app, the row never appeared in
the account's list, and the form had come up without its visibility picker
— the two symptoms of the same thing: the sheet took the local branch.

The destination was decided correctly and then read wrongly. The button
resolved the account and put it in a companion `@State`, and the sheet's
content closure — created by an earlier body evaluation — read that
property as it stood *then*, which was nil. Measured on device: the create
resolved the account and validity fine, and one millisecond later the sheet
rendered the local form while every live value around it still named the
account. Whether the stale read is corrected depends on whether anything
re-renders the view before the form is saved, which is why the same steps
against Piped had landed on the server.

The destination now travels with the presentation as
`PlaylistCreationTarget`, so the sheet is handed the account rather than
looking one up. One resolver answers for both the create affordances and
the row menus, and it is where the rule lives: a context reading an
account's data creates on that account or creates nothing at all — a
toolbar item can outlive the condition that put it there, and a write with
nowhere to go is not a write to somewhere else. A context that fell back to
local reads is the one case that does create locally, and it is honest:
those are the rows on screen, with the banner above them saying why.

Two more from the same pass:

- Subscriptions stopped giving two answers at once. Under a fallback the
  local feed asks for a Yattee Server nobody has configured, so "Add a
  subscriptions account in Settings" was stacking under a banner saying
  this account needs signing in again. The banner names the cause, so the
  feed error stands down while it is up.

- The playlists banner no longer lands on the search field. A navigation
  bar carrying a search field keeps its full height over content that
  cannot scroll, and the empty, loading and error states could not — they
  scroll now, which also gives pull to refresh somewhere to live.
2026-09-05 23:41:25 +02:00
Arkadiusz Fal
175b98097a Put the account switcher where the data is, and retire the old setting
Switching accounts was buried in Subscriptions settings behind a picker
and a confirmation dialog, and it wrote two stores that could disagree.
One shared switcher now sits in the Subscriptions and Playlists
toolbars, the macOS sidebar footer and the Settings root, and every path
goes through AppEnvironment.switchDataContext(to:) so a switch behaves
the same wherever it is made. Switching is one tap: it is reversible, so
there is nothing to confirm.

Rows come from DataContextValidator.availableContexts, name themselves
through displayName(for:), and carry a status suffix when the account is
not signed in — plus the instance host when two accounts share a name.
tvOS gets the same rows as a full-screen picker.

Subscriptions also explains itself when the selected account cannot
answer: a thin banner names the cause and says the data below is local.
This is the first consumer of the validator's unavailability, and
correctIfContextRemoved() now runs on foreground activation, where
SubscriptionAccountValidator used to auto-correct any selection it could
not immediately verify.

With the switcher reading the context directly, the legacy
subscriptionAccount setting has no readers left: the setting, its
validator, the mirror that kept it in step and the feed cache's
instance-keyed fallbacks all go. The cache invalidation the mirror also
carried stays, under its own name. AccountsManager still reads the raw
legacy key for the one-way context migration.
2026-09-03 23:59:32 +02:00
Arkadiusz Fal
9a93b0347e Tell the views when a credential is replaced
Signing in again as a different person on the same server changes
neither the account ID, the context, nor usually the status — so nothing
the views observe moved, and the previous user's subscriptions and feed
stayed on screen while actions went out under the new credential. There
is now a notification for exactly that, and the subscriptions view and
the instance browser discard and reload on it.

An account deleted after its instance disappeared retires its legacy
Keychain item too. Identifying that item needs the instance, but
checking it does not: the adoption marker is specific to service,
account, scope and the exact secret, so asking both managers retires it
if either owns it and does nothing otherwise.
2026-08-28 23:58:20 +02:00
Arkadiusz Fal
1c0a3e8e9a Keep one login's verdicts and content off another's
A feed failure recorded its verdict before checking whether anyone was
still waiting for it, and the verdict resolves the *currently* selected
account — so a 401 from a credential the app had already moved off could
mark its replacement, or a different account, as rejected indefinitely.

The instance browser drops its feed, subscriptions and playlists when
the selected login changes. Nothing else there would notice: the
instance, the tab and the "already has data" shortcut are all unchanged
between two accounts on the same server.

Caches are no longer discarded while the identity is unresolved. With
iCloud Drive off the scope drops to unresolved on every activation and
comes back the same, and the context reads as local in between — which
looked like a move and threw the caches away several times a day.

Local is always in the account picker again. Its feed needs a Yattee
Server and reports that itself; with correction no longer moving away
from local, leaving it out of the list made it unreachable once left.
And the subscriptions view reloads when an instance is switched off,
which moves reads to local without touching the context or any status.
2026-08-28 23:24:06 +02:00
Arkadiusz Fal
e1ebb71511 Bind results to the account and identity they were requested for
Fetched account records are staged under the identity the fetch began
under, not whichever is current when they arrive — the engine's fetch
spans several delegate events and the Apple account can move between
them, which would have filed one identity's accounts under another's.

Subscription fetches are fenced against context switches, not only
against a provider being discarded. Providers are deliberately kept
across a switch — each is one account's cache — so an earlier account's
fetch could finish afterwards and put its channels on screen while
unsubscribes went to the account now selected.

The subscriptions view reloads when the *effective* context moves, which
a credential arriving, expiring or being rejected does without the
stored context changing at all.

Feed requests report what they learn about the account, like every other
authenticated call: a credential that expired during a feed-only refresh
kept looking signed in, and the fallback never engaged. The
classification behind that verdict now exists once, on AccountsManager,
rather than in each provider.
2026-08-28 19:58:27 +02:00
Arkadiusz Fal
ae2fee2d4c Make a second login on an instance reachable and orderable
An additional login could be created but never selected: the legacy
picker lists one entry per instance and always resolved to whichever
account signed in first. Account rows gain a "Use This Account" action,
and the subscriptions view reloads on the data context rather than on
the legacy setting — two logins on the same instance map onto the same
legacy value, so switching between them changed nothing on screen while
writes went to the other account.

Two devices migrating different legacy selections produced contexts with
identical epoch timestamps, and each refused the other's: a permanent,
silent divergence that both sides considered settled. Equal timestamps
are now broken by the selected account's ID, on the incoming side and in
the conflict resolver alike — a record that wins the upload must not be
refused on arrival.

Credential cleanup falls back to both managers whenever the backend
cannot be resolved, not only when the account is unknown here: an
instance deletion can arrive before the tombstone, and the credential
was then left in the Keychain.
2026-08-26 22:59:52 +02:00
Arkadiusz Fal
9065234606 Key subscription providers by account rather than backend type
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.
2026-08-24 23:56:40 +02:00
Arkadiusz Fal
b47888fe04 Add custom accent color with system color picker
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
2026-07-04 23:57:59 +02:00
Arkadiusz Fal
f5b86effd3 Fix layout jump when subscriptions sidebar appears after load on macOS
Load local-account subscriptions synchronously on appear so the channels
sidebar is present in the first layout pass, and reserve the sidebar
column with a spinner while async loads are in flight (gated by a
persisted flag so users without enough channels never see a phantom
column).
2026-07-04 17:04:09 +02:00
Arkadiusz Fal
77720ccea1 Pin view options button to leading toolbar edge on macOS
Place the view options control on the leading (`.navigation`) edge on
macOS so it is consistently the first/leftmost toolbar button across all
views, instead of sitting on the right next to the search field in
search views and on the left in non-search views.

Each affected view gains a computed `viewOptionsPlacement` (`.navigation`
on macOS, `.primaryAction` elsewhere) so iOS/iPadOS/tvOS keep the
button trailing. Search-style views (Search, InstanceBrowse) declare the
view options item first so it renders left of the search-filters button;
their flexible spacer is kept to right-pin the search field. Channel's
fixed spacer between view options and the channel menu is now iOS-only.
2026-06-14 16:03:10 +02:00
Arkadiusz Fal
271aff86b5 Present view options and search filters as popovers on macOS
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
2026-06-06 22:59:36 +02:00
Arkadiusz Fal
f80ba26277 Enforce minimum 2 grid columns on tvOS 2026-05-08 20:04:29 +02:00
Arkadiusz Fal
4f763373c1 Fix tvOS pickers 2026-05-08 02:18:25 +02:00
Arkadiusz Fal
823faee012 Add Show Sidebar toggle to tvOS Subscriptions view
Mirrors the existing iOS/macOS option using the shared
subscriptionsShowSidebar AppStorage key. When the sidebar is hidden
the View Options button moves above the feed so it remains reachable.
2026-05-07 06:57:06 +02:00
Arkadiusz Fal
6df80c0e79 Use user-selected accent color in Home, Subscriptions, and Downloads
Color.accentColor and .foregroundStyle(.tint) resolve to the asset
catalog accent on macOS, so Home shortcut cards, section header
links, the Subscriptions "All channels" header, and the Downloads
per-channel group headers stayed blue when the user picked a
different accent. Read the color from SettingsManager and apply it
directly, matching the pattern already used for the Play button.
2026-04-23 22:55:48 +02:00
Arkadiusz Fal
e0e1e8cbd7 Add resizable subscriptions sidebar on iPad 2026-04-20 18:16:23 +02:00
Arkadiusz Fal
8ff5eccca9 Persist subscriptions sidebar width on macOS 2026-04-20 07:56:56 +02:00
Arkadiusz Fal
ad075319ee Tweak Subscriptions view options sheet layout 2026-04-19 18:26:51 +02:00
Arkadiusz Fal
31b244880b Add Show Sidebar toggle to Subscriptions view options
Adds a Show Sidebar toggle (iPad regular and macOS) that controls
channels sidebar visibility. When the sidebar is shown, the channel
strip picker is disabled and the redundant channel header link above
the feed is hidden. Layout picker now uses inline menu style for
consistency with other options.
2026-04-19 18:03:53 +02:00
Arkadiusz Fal
fe78261866 Add channels sidebar to Subscriptions on iPad regular width
On iPad in regular horizontal size class, the Subscriptions view now
shows a channels column next to the feed (mirroring macOS/tvOS) instead
of the floating channel strip. iPhone and compact-width iPad keep the
existing strip.

- Renames MacSubscriptionsSidebarRow to SubscriptionsSidebarRow and
  shares it across macOS and iOS.
- Uses a custom ScrollView-based sidebar on iPad to avoid iOS 26's
  sidebar background extension bleeding into the selection highlight.
- Forces inline toolbar title on iPad regular so scrolling either
  column behaves consistently.
2026-04-19 17:48:17 +02:00
Arkadiusz Fal
5e205e4a4c Use resizable sidebar layout for Subscriptions on macOS
Replaces the iOS-style floating channel strip with a dedicated
channels column next to the feed, using HSplitView for a native
draggable divider. Mirrors the tvOS two-column structure.
2026-04-19 15:04:21 +02:00
Arkadiusz Fal
181cf2f73a Fix collapsed sheets on macOS across the app
Add macOS-only minimum frame sizing to sheets that wrap a
NavigationStack/Form without intrinsic size, so they render properly
instead of collapsing to just the toolbar. Affects Customize Home,
subscription/channel view options, playlist create/edit, search
filters, media browser options, instance picker, log filters, preset
editor, and legacy data import result.
2026-04-19 11:33:16 +02:00
Arkadiusz Fal
a0015086a2 Use menu picker style for tvOS view options sheets 2026-04-18 20:38:02 +02:00
Arkadiusz Fal
6090454707 Use segmented picker style for tvOS grid columns count 2026-04-18 20:38:02 +02:00
Arkadiusz Fal
f8ca23308d Show grid columns option in view options on tvOS
Expose the grid columns picker in SubscriptionsView and ManageChannelsView
inline sheets, and track viewWidth in ChannelView's tvOS GeometryReaders so
the shared ViewOptionsSheet can compute a meaningful column range.
2026-04-18 20:38:01 +02:00
Arkadiusz Fal
5ef40e24bf Remove channel card background on tvOS and fix grid focus clipping 2026-04-18 20:38:01 +02:00
Arkadiusz Fal
df232ad69a Rework tvOS Subscriptions view as two-column layout
Replace the tvOS Subscriptions header (All Channels link + View Options
button) with a left-column channels sidebar that filters the feed in
place, and move view options into a button at the top of the sidebar.
Drops the channel strip size picker from the tvOS options sheet since
the strip does not apply there, and mirrors the ContinueWatchingView
focus pattern so initial focus and post-filter focus land on the first
video row.
2026-04-18 20:38:01 +02:00
Arkadiusz Fal
033c93e542 Make Subscriptions view focusable on tvOS
Move the channel-link button and View Options into a top safe-area inset
on tvOS so they are reachable with the remote, mirroring the Continue
Watching pattern. Wrap chrome and content in focus sections with a
default-focus namespace so initial focus lands on the first video. Hide
the duplicate in-content section header on tvOS, and add
scrollClipDisabled to VideoListContainer so focus scaling on rows is
not clipped at the scroll edges.
2026-04-18 20:38:01 +02:00
Arkadiusz Fal
8cd3aca96c Generalize Yattee Server credentials manager to BasicAuthCredentialsManager
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.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
f8da242968 Fix ContentUnavailableView centering on Apple TV
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.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
2e37873a12 Remove duplicate navigation titles on tvOS
The sidebarAdaptable TabView already shows tab names in the sidebar
pill, so the large .navigationTitle() was redundant on tvOS.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
100df744d9 Yattee v2 rewrite 2026-04-18 20:37:24 +02:00