Commit Graph

781 Commits

Author SHA1 Message Date
Arkadiusz Fal
1f7c471fcc Give the tvOS media-source-not-found screen an Open Sources button
A media source destination whose source was removed rendered a bare
ContentUnavailableView, so the remote had nothing to focus.
2026-10-16 23:59:09 +02:00
Arkadiusz Fal
9002b253d0 Give the tvOS downloads placeholders a focusable Done button
yattee://downloads still reaches the downloads destination on tvOS,
where it renders a bare ContentUnavailableView with nothing focusable,
leaving the remote stuck.
2026-10-16 23:58:58 +02:00
Arkadiusz Fal
b85243852a Give tvOS Opened Links a focusable Done while no video has resolved
The batch buttons are disabled until a video resolves, and loading or
failed rows are not focusable, so with every link still loading or
failed the remote was stuck.
2026-10-16 23:58:54 +02:00
Arkadiusz Fal
5390f99b82 Give tvOS playlist not-found and empty states a focusable button
The not-found state had nothing focusable, and an empty remote playlist
this account does not own showed no left-column buttons either, so the
remote was stuck. Not found offers Retry (remote) or Open Playlists
(local); the empty remote playlist offers Refresh.
2026-10-16 23:55:14 +02:00
Arkadiusz Fal
3875c864b1 Show a failed remote playlist load as an error with Retry
A remote playlist arrives with its title, so the body always rendered
playlistContent and the errorView branch was never reached: a failed
load showed the empty-playlist placeholder and no way to retry.
2026-10-16 23:30:21 +02:00
Arkadiusz Fal
d5b5b3262d Give the empty tvOS legacy import screen a focusable Done button
The toolbar Done is iOS/macOS only, so after the last legacy account or
source was imported or removed the tvOS screen had nothing focusable and
the remote was stuck.
2026-10-15 23:57:50 +02:00
Arkadiusz Fal
2c3216e048 Give the empty tvOS Continue Watching screen a focusable Search button
With nothing in progress the screen showed only a ContentUnavailableView,
leaving the focus engine nowhere to land and the remote stuck. Search is
the target because it always has a focusable field.
2026-10-15 23:56:33 +02:00
Arkadiusz Fal
164f828098 Give the empty tvOS Accounts screen a focusable Open Sources button
With no accounts the screen showed only a ContentUnavailableView, so the
focus engine had nothing to land on and the remote could not reach the
sidebar again; the app had to be force-quit. Sources is also where an
account is signed in.
2026-10-15 23:51:07 +02:00
Arkadiusz Fal
4c31c0a348 Remove unused AppSettings, Instance and MediaSource CloudKit record types
Nothing creates or reads these records, and neither the development nor
the production CloudKit schema has these types.
2026-10-15 20:44:00 +02:00
Arkadiusz Fal
f2eacaf6d6 Give each thread its own libsmbclient talloc stackframe
One build 270 crash log aborts in talloc (talloc_abort_unknown_value)
inside libsmbclient name resolution while a media-source download opens
a file through SMBBridge, with FFmpeg reading an smb:// file on another
thread at the same time.

libsmbclient keeps its talloc stackframe behind a table of thread
functions, and none was installed (this build does not export
smbc_thread_posix()), so every thread shared one stackframe and the two
users freed each other's allocations. Install a pthread implementation
of smb_thread_functions at load time, before any libsmbclient use,
including FFmpeg's. The struct layout matches what talloc_stack and
smb_threads in the bundled library read (get_tls at +0x30, lock_mutex
at +0x10).
2026-10-14 22:30:34 +02:00
Arkadiusz Fal
7d769f3541 Skip mpv renders that land after the app went to the background
Four build 270 crash logs abort inside the GL driver
(GLDContextRec::flushContextInternal) from presentRenderbuffer, reached
from the immediate render queued by appDidBecomeActive/resumeRendering.
Only displayLinkFired checked isInBackground; renders dispatched
directly to the render queue could run, or present, after the app was
backgrounded again, where GPU work is not permitted.

Check isInBackground at the top of performRender and again before
presenting. PiP rendering is unaffected.
2026-10-14 20:55:41 +02:00
Arkadiusz Fal
eeee74f35f Stop the progress save from getting the app killed at suspension
17 of the build 270 crash logs are 0xdead10cc kills on the main thread
inside DataManager.save(), every one reached from the 5-second
PlayerService progress-save timer. During background audio the timer
keeps writing; when playback stops the app can be suspended mid-save,
still holding the SQLite lock in the shared app-group container.

Wrap the periodic save in a background-task assertion when the app is
not active, and skip the write when the position has not moved since
the last save, so a paused player stops writing every five seconds.

The remaining six 0xdead10cc logs die in DataManager.init recovering
the WAL during a background launch. They are most likely the aftermath
of these dirty kills, so store opening is left as is.
2026-10-14 18:08:32 +02:00
Arkadiusz Fal
e5cea1b6c8 Fix a crash opening Search when a video is in history under several sources
watchEntriesMap() built its dictionary with uniqueKeysWithValues, but the
same video ID can be recorded under more than one source scope, which
trapped on the duplicate key (3 crash logs from build 270, all in
SearchView.loadWatchEntries). Keep the most recent entry instead.

The subscription metadata map in Manage Channels had the same shape (a
channel subscribed under several account scopes), as did the server
history hydration map; both now tolerate duplicates.
2026-10-14 09:24:56 +02:00
Arkadiusz Fal
fd6a97ce5c Keep the video info carousel aligned across window resizes
The carousel's lazy stack kept the old widths of cards it had laid out
off-screen, so after a resize (tab bar to split view sidebar on iPad,
rotation) every later card sat at a stale offset and the current card
landed beside the viewport. The stack now takes a new identity per
card width, and the existing re-snap aligns the current card.

Swapping between the tab bar and the sidebar also rebuilds the page,
which reset the carousel to the video it was opened on. The page's
position and continuation-loaded videos now live on its queue context,
fresh for every push, so a rebuilt page resumes where it was.
2026-10-14 08:22:58 +02:00
Arkadiusz Fal
97288b4497 Keep the split view mini player on pushed pages
The detail column absorbs its NavigationStack, so the mini player inset
on the stack only reached the root page and disappeared on push. Float
it over the split view, past the sidebar, and have every page reserve
its height as bottom safe area.
2026-10-13 23:58:49 +02:00
Arkadiusz Fal
f289905dd8 Keep the landscape panel hidden where the corner layout doesn't fit
The corner preset's landscape fallback opened the panel whenever the
device could host the corner layout. On the iPhone Duo exterior display
the player falls back to the widescreen layout, so the floating panel
opened on every rotation into landscape. Resolve the fallback from
whether the corner layout fits the current size instead.
2026-10-13 23:57:58 +02:00
Arkadiusz Fal
b900c7baad Keep the player top bar at the edge beside a corner cutout
On the iPhone Duo exterior display in landscape with the camera in the
top corner, the window's 82pt top safe area comes only from the camera
and status items block, and dropped the whole top bar below it. When
everything occluding the top safe area sits in one corner, the top bar
stays at the edge and the corner adaptation offset clears the block.

The top bar no longer adds its own leading cutout clearance on top of
that offset, and cutouts only count when they reach into a bar's row.
2026-10-13 23:57:41 +02:00
Arkadiusz Fal
decaec7026 Keep player controls clear of the camera cutout
On the iPhone Duo exterior display in landscape the camera cutout sits
in the bottom trailing corner, outside the safe area, and covered the
rotate button and the end of the progress bar. The top and bottom bars
now take extra padding on the side where an occlusion reserved region
reaches into them. Regions spanning the middle, like a portrait Dynamic
Island, are left to the safe area.
2026-10-13 23:41:32 +02:00
Arkadiusz Fal
7f4555dc2e Keep the macOS corner player layout from being squeezed
The separate player window could be resized below what the corner layout
needs. Free resize now floors at the corner layout's minimum (its width
threshold, the narrowest side column plus a video column wide enough for
the control bar, and 600pt tall), and an undersized window grows to it
when the corner layout takes over. The video column minimum is 560pt on
macOS so the control bar capsule fits.

The control bar's fixed 650pt frame also widened the controls stack past
a narrower video column, clipping the top bar's avatar and close button;
the frame is now capped to the container width.
2026-10-13 23:18:47 +02:00
Arkadiusz Fal
a2ee7d5363 Keep the player pill off the hinge in book posture
On a foldable with the hinge partially open, the corner layout's player
pill moves to the center of the trailing half while the side column is
hidden. It slides with the column as it is dragged away or revealed,
keeping its width.
2026-10-13 19:01:43 +02:00
Arkadiusz Fal
a6dd02e934 Disable the vertical status bar in the player window
On the iPhone Duo exterior display the vertical status bar reserved an
84pt trailing column beside the video. Opt the player window out so the
status bar stays horizontal and the video uses the full width.
2026-10-12 22:48:36 +02:00
Arkadiusz Fal
77c3103748 Keep player buttons off the hinge in book posture
On a foldable with the hinge partially open in landscape, the fullscreen
player's buttons move to the half right of the hinge. The dimming
background and tap handling still cover the whole screen.
2026-10-12 22:03:02 +02:00
Arkadiusz Fal
23d6a605a0 Show an empty-queue placeholder in the queue panel page
When nothing is queued and the history and now-playing sections are
hidden or empty, show the same placeholder as the queue sheet instead
of a blank list.
2026-10-12 21:21:24 +02:00
Arkadiusz Fal
4a9a92d140 Add a Currently Playing toggle to the queue panel page
Below Previously Played in the queue page settings; off by default, so
the queue page lists history and up next without the playing video.
With it off the page opens scrolled to Up Next.
2026-10-12 19:45:02 +02:00
Arkadiusz Fal
e71bb76f13 Default the built-in presets to the corner layout
The built-in presets now use the corner layout, which still falls back
to stacked where it doesn't fit (iPhone, iPad portrait, narrow windows).
In the corner layout the landscape panels open visible unless the preset
says otherwise, on devices that can show it: iPad, macOS, and foldable
iPhones, whose hinge is now remembered once reported. Custom presets and
presets saved without panel settings keep the stacked layout.
2026-10-12 18:24:46 +02:00
Arkadiusz Fal
6c7c0772fd Show the Foldable playback settings on hinged devices
The section carried its own hinge observer but rendered nothing until a
hinge was reported, so the observer never attached and the section never
appeared. Observe the hinge on the always-present form instead.
2026-10-11 23:57:38 +02:00
Arkadiusz Fal
ac88912316 Ask Play Now/Play Next when a video is playing with an empty queue 2026-10-11 22:35:18 +02:00
Arkadiusz Fal
6e8835237d Add a Navigation setting to keep the tab bar on large screens
iPad and iPhone Duo users can pick Tab Bar instead of Automatic to opt
out of the sidebar layout. Switching rebuilds the navigation Settings
lives in, so the change asks for confirmation and lands on Home first.
2026-10-11 21:28:51 +02:00
Arkadiusz Fal
2c662b34e3 Hide Shorts in Invidious Popular too
Invidious serves /api/v1/popular from the same RSS-fed table as the
subscription feed, so Shorts there arrive with no length as well.
2026-10-11 19:19:51 +02:00
Arkadiusz Fal
8ade5a54d0 Add a Hide Shorts toggle to the view options
Videos carry isShort from Yattee Server and Piped, and Invidious feed
entries are taken as Shorts when they arrive with no length and aren't
live or upcoming: the feed comes from RSS, which lists Shorts, while
lengths come from the Videos tab, which doesn't. Subscriptions, search
and instance browse hide videos known to be Shorts when the toggle is
on; videos whose source can't tell are always shown.
2026-10-11 14:06:15 +02:00
Arkadiusz Fal
2d3ae1b0e5 Block the player dismiss pan from starting on side sliders
The window-level dismiss pan recognizes before SwiftUI's slider
DragGesture sets isAdjustingPlayerSliders, so dragging volume or
brightness often dismissed the player instead. Side sliders now publish
their frames and the pan refuses to begin from touches inside them.
2026-10-10 23:58:16 +02:00
Arkadiusz Fal
cf1acb1986 Apply volume and mute on the avfoundation renderer directly
mpv's volume/mute properties are software volume applied before samples
reach the AO, and AVSampleBufferAudioRenderer queues ~2s ahead, so slider
and mute changes were heard only after the queue drained. With the
avfoundation AO active, drive ao-volume/ao-mute (renderer volume, instant)
and keep software volume neutral; other AOs keep software volume. Re-apply
on every audio reconfig since a new renderer starts at full volume.
2026-10-10 23:58:03 +02:00
Arkadiusz Fal
22e94e4f6c Stop double-unscaling the dismiss morph's mini player target
The mini player's SwiftUI global frame already ignores the scale transform
on the main window's content, so mapping it out of that scale again landed
the morph about 8% too large, left and high of the preview, which then
jumped into place when the real mini player took over.
2026-10-10 22:08:29 +02:00
Arkadiusz Fal
8233ac26d7 Collapse the player for Video Info / Go to Channel from rows inside it
Related and queue rows in the player use the default menu context, so the
pushed screen landed underneath the still-expanded player. Collapse on
player state instead of the .player context. Recommendations without an
authorId no longer offer Go to Channel, which opened an empty channel.
2026-10-10 21:27:26 +02:00
Arkadiusz Fal
4510a24d9f Move the channel strip into the Duo status-bar column as a vertical rail
Where the trailing safe-area column is wide enough for avatars (the iPhone
Duo inner screen's vertical status bar), the channel filter becomes a
vertical rail stacked from the bottom of the column instead of a strip over
the feed. The space below the status items and Dynamic Island comes from
iOS 27.1 occlusion reserved regions, which follow the island while the safe
area stays put. The placement is computed in the same pass as the geometry
so the collapsing large title can't drag the rail along for a frame.

Large keeps bigger avatars in the narrow column by giving up padding first.
2026-10-10 19:10:14 +02:00
Arkadiusz Fal
41fb54d278 Land the dismiss morph on the mini player's final position
The mini player reported its frame inside the main window's scaled-down
content, so the morph aimed short of where it ends up once the scale
animates back. The scaled content also had a smaller bottom safe area
(UIKit derives it from the transformed frame) that only came back on the
next layout, lifting the mini player 2pt after the morph landed. Map the
target out of the scale, account for the safe area it gets back, and
re-lay the main window out as the morph starts.
2026-10-10 11:49:38 +02:00
Arkadiusz Fal
09a384763a Stretch the mini player's video preview to fill its frame
Wide videos showed letterbox bars in the tiny iOS mini player preview right
after the dismiss morph, which lands the video filling the preview. The
container that takes the shared player view now sets mpv's keepaspect, so
the mini player fills its frame and every other surface keeps the aspect
ratio.
2026-10-09 23:59:58 +02:00
Arkadiusz Fal
7b1a24a5b0 Morph the expanded player into the mini player on drag-to-dismiss
Releasing the dismiss drag now shrinks the player window onto its video and
flies it into the mini player's preview instead of sliding it off screen and
showing the mini player afterwards. The mini player stays mounted under the
expanded player and reports its preview frame, and the window is masked down
to the video while its root view is transformed onto that frame (with the
safe area the transform drops added back, so the content doesn't jump).

The handoff of the shared MPV view is made seamless: a freshly made player
container waits for its first layout before taking a view that is on screen
elsewhere, sizes and redraws it there, and the window hides in that same
frame. Resizing the render view draws the current frame into the recreated
framebuffer right away instead of leaving it blank until the next tick.

Falls back to the slide when no preview target is available (rotated
session, video preview hidden, different window size).
2026-10-09 23:59:51 +02:00
Arkadiusz Fal
cf8d477351 Inset the mini player into the split view's detail column
The floating overlay ignored the safe area, so the subscriptions channel
strip sat under it on the iPhone Duo inner screen and iPad.
2026-10-09 23:59:41 +02:00
Arkadiusz Fal
6561c664e9 Keep the portrait video on the upper half while the hinge is partially open
With the panel shown, the panel now takes the whole lower half with its
top edge on the hinge, and the video stays centered in the upper half
where it sits with the panel hidden, so toggling the panel never moves it.
2026-10-09 23:59:40 +02:00
Arkadiusz Fal
aaa5082d96 Update extracted localizable strings 2026-10-09 23:35:56 +02:00
Arkadiusz Fal
fe109394df Split the corner layout on the hinge when partially open
On a half-open foldable the side column takes exactly the trailing half,
so the split lands on the hinge. Fully open, the column can now be
resized up to half the width. A re-presented player seeds the hinge
state from the last report so it doesn't animate into the split.
2026-10-09 23:19:39 +02:00
Arkadiusz Fal
4d690d6b71 Keep the video info carousel aligned when its width changes
Unfolding an iPhone Duo kept the old scroll offset, leaving the selected
card misaligned. Re-snap to the current card on width change, and clear
the requested position once it settles so the lazy stack doesn't pull
the next swipe back halfway between cards.
2026-10-08 23:59:53 +02:00
Arkadiusz Fal
82236967b7 Show player toasts and drop the unused instance status
PlayerService's toast manager was never set, so stream refresh, missing
download and skip-to-next toasts were silently dropped. Wire it up in
AppEnvironment.

InstancesManager's per-instance status was never written since the v2
rewrite; account statuses already cover rejected and missing credentials
in the sources list, so remove it along with its unreachable row branch.
2026-10-08 23:59:40 +02:00
Arkadiusz Fal
4770bbe3b9 Remove dead code found by Periphery on all platforms
Delete declarations that Periphery reports unused on every platform that
compiles them (iOS, macOS, tvOS), including nine views used only by their
own previews. Platform-gated code still used on another platform, device-
only MPV render paths, migration models, CloudKit record names, App
Intents metadata and unused parameters are kept.
2026-10-08 23:57:56 +02:00
Arkadiusz Fal
45e19e6536 Open more YouTube link types and playlists from links
Route YouTube search results links to search and nocookie embeds to the
video, match YouTube hosts exactly, and report YouTube pages without a
video as unsupported instead of sending them to yt-dlp. A watch link
inside a playlist shows both the video and the playlist when loaded, and
plays the video with the playlist queued around it. URL scheme play,
queue and bookmark actions accept playlist links.
2026-10-08 22:50:36 +02:00
Arkadiusz Fal
8a7218ed1b Show several opened links as mixed results
Load All in Open Link opens an Opened Links page that resolves each link
into a video, playlist or channel row like search results, in input
order. Video rows keep the usual context menu and swipe actions, and the
page can play, queue, bookmark or download all resolved videos.
2026-10-08 22:36:57 +02:00
Arkadiusz Fal
03be964c66 Load a single link from Open Link into its details screen
Open Link gets a Load action that resolves one link and shows its video
details page, playlist or channel screen instead of only playing or
downloading it. Play and Download stay as shortcuts for video links.
Timed links start at their timestamp from the details page, the Show
Details link action opens the details page, and a short extraction cache
avoids re-extracting the same link for details and playback.
2026-10-08 18:25:32 +02:00
Arkadiusz Fal
b9e7d88a12 Re-fetch everything from iCloud when the local store was recreated
On tvOS SwiftData keeps the store in the app group's Library/Caches,
which the system may purge. The CKSyncEngine tokens live in UserDefaults
and survive, so the next launch opened an empty store and fetched only
records changed since the old tokens; subscriptions and bookmarks that
had not changed since never came back.

DataManager now records whether the store file existed when the
container opened, and the sync engine clears its persisted state once
per launch when it did not, so the first sync after a loss is a full
fetch. Also log the store path and a persistent-store failure that
previously fell back to in-memory storage silently.
2026-10-07 23:59:56 +02:00
Arkadiusz Fal
bc6ad55f38 Ask link action confirmations in an alert on tvOS
A tvOS sheet fills most of the screen, far too much for a two-button
question. The shared sheet host now presents link action confirmations
as an alert there, answering cancel when it goes away without a choice.
2026-10-07 23:59:44 +02:00