Commit Graph

50 Commits

Author SHA1 Message Date
Arkadiusz Fal
6e5714dd86 Fix tvOS MPV startup playback stability 2026-05-10 15:28:12 +02:00
Arkadiusz Fal
82d2830208 Refresh track list when advancing to next queued video
Player settings on tvOS showed the previous video's tracks after queue
advance because availableStreams was never cleared on a video change and
playQueuedVideo only seeded a single pre-resolved stream. Reset the list
on a new video, then fetch the full streams in the background for global
videos, and repoint state.currentStream/currentAudioStream to the
refreshed entries so the picker checkmarks land on the playing tracks.
2026-05-10 15:28:11 +02:00
Arkadiusz Fal
6a343311ea Add tvOS display frame rate and dynamic range matching
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.
2026-05-10 15:28:11 +02:00
Arkadiusz Fal
100e762d4b Suppress stale player error after switching videos mid-retry
If the MPV backend was retrying a failed load and the user switched to
another video before retries exhausted, the eventual error was published
to the player UI even though that video was no longer active. Guard the
catch block with a loadingVideoID check so stale errors are dropped.
2026-05-10 15:28:11 +02:00
Arkadiusz Fal
d49591eaf4 Skip local-folder watches from iCloud sync 2026-05-09 15:53:54 +02:00
Arkadiusz Fal
c64f13a0e6 Show cached channel header on tvOS while channel loads
Render subscriber count, Subscribe button, and (for subscribed
channels) description in the tvOS loading state instead of just
avatar + name + spinner. Seed the in-memory author cache when
navigating to a channel from a video so the first-time channel
view has a name and avatar to display immediately.
2026-05-09 15:00:53 +02:00
Arkadiusz Fal
1f0f3a8cf0 Resume and seek when reopening currently-loaded video
When the same video was already loaded (typically paused), opening it
again via the URL scheme, a deep link, or a remote-control loadVideo
command did nothing — the player just stayed paused. Now the same-video
early-return path resumes playback if paused and seeks to the supplied
startTime, so timestamps from URLs and remotes are honoured even when
the video is already loaded.

URLRouter gains a parseTimestamp helper that reads t/time/start query
params in plain-seconds and YouTube-style (1h2m3s) forms, and the deep
link handler now forwards that timestamp through to openVideo.
2026-05-09 15:00:08 +02:00
Arkadiusz Fal
aabf5313fa Expose Background Playback toggle on tvOS, default off
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.
2026-05-09 14:50:38 +02:00
Arkadiusz Fal
42621b8193 Suppress tvOS Now Playing while AirPlay/HomePod route is active
On tvOS, registering MPRemoteCommandCenter handlers makes the system
classify the app as a long-form video media app. When audio is routed
to AirPlay 2 endpoints (HomePods), the system then enforces a ~2s
look-ahead buffer in the AVAudioSession → AirPlay 2 pipe for
multi-speaker sync. The result is a 2-3 second audio drain on pause
and refill on resume.

The buffer lives downstream of mpv, so no mpv command (ao-reload,
seek-flush, audio-add) can flush it; AVAudioSession setCategory
overrides (mode/policy) and setActive(false)/setActive(true) cycles
are also ignored once the app is media-classified.

Workaround: detect the active audio route via routeChangeNotification
on tvOS. While AirPlay/HomePod is the output, suppress
MPNowPlayingInfoCenter publication and disable every MPRemoteCommand
so tvOS un-classifies us. When the route returns to local outputs,
republish the cached Now Playing info and reconfigure remote commands.
A latch defers all media integration until the audio session has been
activated at least once, so no commands are registered before the
route can be evaluated.

Trade-off: while playing to HomePods, the Control Center widget and
external Siri Remote play/pause are not available — but pause/resume
is responsive.
2026-05-09 14:18:17 +02:00
Arkadiusz Fal
5ab9e3d5bf Surface mpv error details on stream load failure
Subscribe to mpv log messages and capture END_FILE error code/string so
load failures bubble up specific causes (HTTP 404/403, DNS failure,
demuxer errors) instead of a generic 10s timeout.
2026-05-08 20:43:27 +02:00
Arkadiusz Fal
b7b7c5ac62 Fix local folder playback after app container UUID changes
After iOS reinstall/restore the app container UUID rotates, which left both
the persisted source.url and the security-scoped bookmark pointing at a
no-longer-current path. Files derived a stale absolute path that got appended
onto the resolved bookmark, producing doubled URLs that MPV could not load.

- Resolve the base URL by picking whichever of the bookmark or source.url
  actually exists on disk.
- Compute MediaFile relative paths against the resolved root so they survive
  later container changes.
- Hold the security-scoped resource access for the source's lifetime via a
  shared resolver, so MPV can open files long after the directory enumeration
  that resolved the bookmark has returned.
- Normalize legacy absolute paths embedded in old recents/history video IDs
  so they re-resolve under the current container.
2026-05-08 18:23:16 +02:00
Arkadiusz Fal
b163864628 Add interactive swipe-to-dismiss for iOS toasts
Toast cards now follow the finger upward and dismiss on either a
sufficient drag or a fast flick (via predicted-end translation). The
auto-dismiss timer pauses while the user is dragging and re-arms if
they release without dismissing.
2026-05-08 03:04:32 +02:00
Arkadiusz Fal
158d518e3a Trim comments and hoist settings read in stream filtering
Drop comments restating what the code shows; hoist allowSoftwareDecodedFormats
out of the recommendedVideoStreams filter closure so the bridge property is
read once per render instead of once per stream.
2026-05-07 18:03:34 +02:00
Arkadiusz Fal
16477641ab Add Allow Software-Decoded Formats playback setting
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.
2026-05-07 18:00:14 +02:00
Arkadiusz Fal
411fcba037 Surface clearer error when adding a Piped frontend URL
Pointing AddRemoteServer at a Piped Vue SPA (e.g. the frontend host
rather than the API host) used to fail with a generic
"could not detect instance type" — every JSON probe got the same
index.html back. On the failure path, fetch `/` once more and look
for `<title>Piped</title>`; if matched, return a new
`pipedFrontendDetected` error that tells the user to enter the
Piped API URL instead.
2026-05-06 21:14:50 +02:00
Arkadiusz Fal
11841d7b41 Send Piped session token in Authorization header again
Commit aed78c13f moved the session token from the Authorization header to a
?authToken= query parameter on /subscriptions, /subscribe, /unsubscribe,
/user/playlists, and /playlists/{id}. Piped's backend only accepts the
?authToken= form on /feed; every other authenticated route reads it from
Authorization (the Java handler names the variable "session", which is why
the rejected requests returned "session is a required parameter"). Restore
the header form for those five routes and leave feed alone.
2026-05-06 20:17:09 +02:00
Arkadiusz Fal
fac297e4d6 Cache and prewarm Invidious proxy auto-detection
The proxy auto-detect path (when proxiesVideos is off) HEADed a
googlevideo URL with a 5 s timeout on every video. The verdict is a
property of the network, not the video, so the cost was paid for no
reason on videos 2..N. On a network where the CDN is blocked the full
5 s timeout was added to playback startup every single time.

Two changes:

1) ProxyDetectionCache (actor, per-instance, 10 min TTL). First miss
   pays the HEAD once and caches the verdict; subsequent videos hit
   the cache synchronously. Concurrent callers share one in-flight
   probe. The last-seen sample CDN URL is retained so future probes
   don't need a fresh URL from the current video.

2) PlayerService kicks off InvidiousAPI.prewarmProxyDetection() in
   parallel with the videoWith... API call. By the time streams come
   back, the verdict is usually already cached and proxyStreamsIfNeeded
   is a sync lookup. Cheap when there's nothing to prewarm.

Cache invalidation:
- on InstancesManager.update (URL change, proxy toggle flip)
- on InstancesManager.remove
- TTL covers the network-change case for now (no NWPathMonitor yet)
2026-05-04 08:04:55 +02:00
Arkadiusz Fal
93240b4314 Wire Yattee Server playback through /proxy/relay when proxiesVideos is on
The Sources -> Edit Source -> Proxy toggle now renders for Yattee
Server entries (supportsVideoProxying gains .yatteeServer). When the
toggle is on, playback fetches go through
videoWithProxyStreamsAndCaptionsAndStoryboards with mode .relay, so
the server returns signed /proxy/relay URLs (byte-relay, supports
HTTP Range, no on-disk caching). Downloads keep going through mode
.download so the server-side /proxy/fast/ flow continues to cache
files for repeat use.

InvidiousAPI.proxyStreamsIfNeeded early-returns for .yatteeServer
since proxying is now done at fetch time via ?proxy=true rather than
client-side host rewriting.
2026-05-04 08:01:51 +02:00
Arkadiusz Fal
fd0eab7784 Prefetch fresh video thumbnail before swapping it into info view 2026-04-23 18:37:19 +02:00
Arkadiusz Fal
664eeadba2 Stabilize Nuke cache key across rotating thumbnail URL tokens 2026-04-23 18:22:03 +02:00
Arkadiusz Fal
5a839da1bd Resolve URL shorteners and prompt for ambiguous description links
Tapping bit.ly/tinyurl/t.co/etc. in a description or comment previously
opened Safari even when the destination was a playable YouTube URL.
Added an opt-in "Resolve Short Links" toggle under YouTube Enhancements
(off by default) that follows the redirect on tap: if the target is a
YouTube/PeerTube/direct-media URL, open it in-app; otherwise prompt the
user before falling back to yt-dlp extraction or the browser.

Also added a confirmation dialog for non-shortener links that only
matched the loose .externalVideo yt-dlp fallback, so arbitrary web
pages in descriptions no longer silently kick off extraction.

Prompts live on NavigationCoordinator and are dual-hosted by YatteeApp
and ExpandedPlayerSheet so they remain visible whether or not the
expanded player is covering the main view.
2026-04-23 07:29:57 +02:00
Arkadiusz Fal
a2a4691957 Integrate Sparkle auto-updates for macOS Developer ID builds
New Release-DeveloperID configuration gates Sparkle behind a SPARKLE
compile flag so the App Store Release build stays Sparkle-free. Adds
SPUStandardUpdaterController wrapper, Check for Updates menu command,
Advanced Settings section with beta channel toggle, and a Ruby script
plus GitHub Actions job that signs each release and publishes the
appcast to gh-pages for consumption by Sparkle and Homebrew cask.
2026-04-23 04:51:00 +02:00
Arkadiusz Fal
b54c32edad Route YouTube links tapped in descriptions through in-app playback
Description links to YouTube videos, channels, playlists, and external
video URLs now open in Yattee instead of Safari. When a video is
already playing, tapping a video link surfaces the existing
QueueActionSheet (Play Now / Play Next / Add to Queue) — the sheet is
hosted both at the app root and inside ExpandedPlayerSheet so it
appears above whichever layer is on screen.
2026-04-22 19:00:43 +02:00
Arkadiusz Fal
096df34f64 Redesign tvOS player controls with centered transport cluster
- Move Close to a top-right circular icon; bottom row reorganizes into
  left (Settings/Info/Comments), center transport (Previous/PlayPause/Next),
  and right (Queue) clusters with equal side frames so transport stays
  geometrically centered.
- Introduce a circular icon-only `TVTransportButtonStyle` (primary variant
  for Play/Pause) mirroring the new Close button look.
- Always render Previous/Next so Play/Pause position is fixed; dim and
  disable when unavailable.
- Share `isTransportDisabled` on `PlayerState` and reuse it on iOS and
  tvOS; apply it (plus symbol replace transition) to the tvOS Play/Pause
  button.
2026-04-18 20:38:02 +02:00
Arkadiusz Fal
2761fcbcfb Replace onboarding flow with silent v1 import and iCloud alert
Delete the multi-page onboarding sheet. On first launch the app now
silently imports any v1 instances from UserDefaults (splitting embedded
basic-auth credentials out of the URL and into the Keychain) and then,
if the device is signed in to iCloud, shows a single alert offering to
enable sync. Accepting shows a blocking progress overlay until the
initial upload completes.
2026-04-18 20:38:02 +02:00
Arkadiusz Fal
9f86ff0667 Add tvOS Top Shelf extension
Surfaces Continue Watching, Recent Feed, and Recent Bookmarks in the
Apple TV Home top shelf when Yattee is focused. Tapping a tile opens
the video via the existing yattee://video/{id} deep link.

- New YatteeTopShelf app extension target (tvOS only). LD_ENTRY_POINT is
  overridden to _NSExtensionMain; the tv-app-extension product type
  defaults to _TVExtensionMain which is for the pre-tvOS-13 legacy API
  and crashes modern TVTopShelfContentProvider subclasses at launch.
- Main app writes per-section JSON snapshots (capped at 10 items each)
  to a shared App Group UserDefaults suite after bookmark, watch-history,
  and feed-cache changes, plus an initial write on launch.
- Enabled-sections list is mirrored to the same App Group so the
  extension can respect the user's selection without touching SwiftData.
- Settings → Top Shelf (tvOS only) lets the user toggle sections.
- Deep link playback shows a loading toast while video details are
  fetched, and an error toast if no source is configured.
2026-04-18 20:38:02 +02:00
Arkadiusz Fal
2efa0708c8 Refresh expired thumbnail URLs for downloads and video info
Proxied thumbnail URLs from Invidious/Piped/Yattee server expire over
time. Two paths were left holding stale URLs: the Video Info carousel
kept the original list copy even after fresh details arrived, and
downloaded videos rendered from the remote URL snapshot taken at
download time while the local thumbnail on disk was ignored.

Evict stale URLs from the Nuke cache when fresh video details load,
pass the fresh details through to the videoCard thumbnail, and resolve
downloads' thumbnails from the local file when localThumbnailPath is
set.
2026-04-18 20:38:02 +02:00
Arkadiusz Fal
82a8ac2afa Fix storyboard downloads with yattee-server direct YouTube URLs
yattee-server returns direct YouTube CDN URLs in the storyboard `url`
and `templateUrl` fields instead of an Invidious-style VTT proxy path.
Two resulting issues:

- `Storyboard.directSheetURL` was replacing the whole `M$M` token with
  just the index, producing `.../0.jpg` (404) instead of `.../M0.jpg`.
  Replace `M$M` with `M\(index)` to preserve the literal `M` prefix;
  matching the full token also avoids clobbering `$M` sequences that
  may appear in `sigh=rs$...` query params.
- The download code fetched `proxyUrl` as if it were a WebVTT file;
  with yattee-server that downloads a JPEG that fails UTF-8 parsing.
  Skip the VTT round-trip when `proxyUrl` obviously points at an image.

Also align the on-disk filename with the local-playback template
(`sb_M$M.jpg` → `sb_M{N}.jpg`) so offline seek-bar previews resolve,
and add [Storyboard] debug logs at each decision point so future
failures can be diagnosed without guessing.
2026-04-18 20:38:01 +02:00
Arkadiusz Fal
c7d1f1c20b Seek with remote arrows when tvOS player controls are hidden
Left/right on the Siri Remote now seek instead of revealing controls,
reusing the iOS tap-seek accumulation handler and feedback overlay so
rapid presses compound into a single "+30s" / "-20s" jump. Up/down
still show controls.
2026-04-18 20:38:01 +02:00
Arkadiusz Fal
b9a6d76ab3 Fix original audio detection so dubbed tracks don't play by default
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.
2026-04-18 20:38:01 +02:00
Arkadiusz Fal
eefd49f743 Fix three basic-auth regressions surfaced by end-to-end testing
- 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.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
222b53d520 Surface 401 from instance detection so the user can supply credentials
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.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
63f1cb1f25 Inject basic auth via per-instance HTTPClient default headers
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.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
aed78c13fb Send Piped auth token via query parameter instead of header
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).
2026-04-18 20:38:00 +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
0071e1b117 Skip notifications for upcoming/premiere videos
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.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
d6d15df105 Deduplicate time formatting and clean up unused code
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().
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
a7e5ebb068 Fix 5 TestFlight crash types from builds 250-254
- 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
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
e956075f3c Fix CFNetwork SIGABRT crash when creating download tasks on invalidated session
The background URLSession could be in an invalid state when downloadTask(with:)
is called, because invalidateAndCancel() is asynchronous internally. This adds
an ObjC exception handler to catch NSExceptions from CFNetwork, nil guards on
the session, and safer session lifecycle management (nil after invalidation,
finishTasksAndInvalidate for cellular toggle).
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
d1e63e85a4 Apply Invidious proxy rewriting to download streams
Downloads were using direct YouTube CDN URLs even when proxiesVideos
was enabled. Apply the same proxyStreamsIfNeeded used by the player
to the download code path in ContentService.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
7c2a205e74 Fix Piped relatedStreams decoding crash on missing fields
Malformed items in relatedStreams (e.g., missing title) no longer crash
the entire JSON decode. Reuses the existing PipedVideoItem (renamed from
PipedPlaylistItem) graceful-decoding wrapper for all relatedStreams arrays
in PipedStreamResponse, PipedChannelResponse, and PipedNextPageResponse.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
6298b38cba Add video proxy support with live toggle for Invidious/Piped instances
Adds a "Proxy videos" toggle in instance settings that routes video
streams through the instance instead of connecting directly to YouTube
CDN. Includes auto-detection of 403 blocks and live re-application of
proxy settings without requiring app restart or video reload.
2026-04-18 20:38:00 +02:00
Arkadiusz Fal
07003a36d7 Fix deleted playlists resurrecting from iCloud after app restart
Pending deletes were lost across app restarts because
recoverPersistedPendingChanges() never reconstructed CKRecord.ID
objects from persisted record names. Additionally, incoming iCloud
records for deleted playlists were blindly applied, and orphaned
playlist items in CloudKit would recreate placeholder playlists.

- Rebuild pendingDeletes array from UserDefaults on recovery
- Guard applyRemoteRecord against records pending local deletion
- Skip deferred items whose parent playlist is pending deletion
- Queue all playlist item deletions when deleting a playlist
- Clean up placeholder playlists for pending-delete playlists
2026-04-18 20:37:35 +02:00
Arkadiusz Fal
49a44da90e Fix Invidious login failing for passwords with special characters
Use URLComponents/URLQueryItem for standard form-URL encoding instead
of manual percent-encoding with CharacterSet.alphanumerics, which
included non-ASCII Unicode letters and had an unsafe raw-value fallback.
2026-04-18 20:37:25 +02:00
Arkadiusz Fal
ef3cddefeb Persist author cache to disk for instant channel info across restarts
Back the in-memory authorCache with a JSON file in ~/Library/Caches/AuthorCache/.
Disk is lazy-loaded on first lookup and saved asynchronously on each cache update.
Capped at 500 entries to prevent unbounded growth.

- Cache author data from video detail API responses (PlayerService, VideoInfoView)
- Replace ChannelView's private CachedChannelHeader with shared CachedChannelData
- Enrich author with cached avatar/subscriber count in VideoChannelRow, TVDetailsPanel, VideoInfoView
2026-04-18 20:37:25 +02:00
Arkadiusz Fal
aaf53ef9d1 Fix lock screen always showing 10s seek regardless of system controls setting 2026-04-18 20:37:25 +02:00
Arkadiusz Fal
f010650e5e Remove excessive logging 2026-04-18 20:37:25 +02:00
Arkadiusz Fal
5f00f4934c Fix player dismiss gesture stuck after panel dismiss with comments expanded
Reset isCommentsExpanded and commentsFrame on the NavigationCoordinator
directly when the portrait panel is dismissed, since PortraitDetailsPanel
owns its own @State that doesn't sync back through .onChange during dismiss.
Also track comments overlay frame via GeometryReader so the dismiss gesture
can allow swipes outside the comments area instead of blanket-blocking.
2026-04-18 20:37:25 +02:00
Arkadiusz Fal
ecaf553326 Fix incomplete playlist loading by paginating through all pages
Playlists only loaded the first page of videos. Add full pagination for
both Invidious and Piped playlist endpoints (public and authenticated).
Deduplicate Invidious results by playlist index to handle its overlapping
page windows. Also fix URL encoding in Invidious login to use strict
form-encoding charset.
2026-04-18 20:37:25 +02:00
Arkadiusz Fal
100df744d9 Yattee v2 rewrite 2026-04-18 20:37:24 +02:00