Commit Graph

649 Commits

Author SHA1 Message Date
Arkadiusz Fal
bebb62068a Scroll mini player marquee with Core Animation on iOS and tvOS
The TimelineView re-rendered both mini player lines every frame (~17% of
the main thread on an iPhone 12 mini, even while paused). The text is now
drawn once in a hosted view and a repeating keyframe animation slides its
layer in the render server. macOS keeps the TimelineView.
2026-10-26 23:13:29 +01:00
Arkadiusz Fal
c09e66cb5a Cut foreground CPU while playing and paused
- Send Now Playing elapsed time once per 5-second bucket; the modulo check
  fired for every frame of that second (12 updates/s on average).
- Skip mpv renders while paused unless mpv requests a redraw (seek, new
  frame, PiP change, new load) instead of redrawing the same frame at
  display rate.
- Read playback time untracked in the progress bar and time labels while
  the controls are hidden, so they are not re-rendered on every tick.
- Only clear the current SponsorBlock segment when one is set.
2026-10-26 19:35:12 +01:00
Arkadiusz Fal
73d491c56b Read the Duo status column's occlusion regions from a full-bleed reader
SwiftUI only reports reserved regions that intersect the querying view,
and the subscriptions content stops at the trailing safe-area column, so
on device the channel rail saw no regions and ran up under the time and
Dynamic Island. A background reader that ignores the safe area now
records the regions in global coordinates for the rail placement.
2026-10-25 16:58:31 +01:00
Arkadiusz Fal
728e3b7286 Cut foreground CPU during audio-only playback
- MPVRenderView keeps its display link paused while the stream has no
  video, instead of rendering and presenting 60 empty frames a second
- Chapter segments get clamped played/buffered fractions and are
  equatable, so a time update re-renders only the chapter under the
  playhead instead of every segment
- The mini player pauses its scrolling titles while the expanded player
  covers it (iOS/tvOS)
- Performance logging no longer flags the first sample after a
  foreground/background transition as high CPU

With the expanded player open in audio-only mode on an iPhone 12 mini,
app CPU dropped from ~30% to ~12% and wakeups from ~128/s to ~37/s.
2026-10-25 14:22:26 +01:00
Arkadiusz Fal
9412ca916c Add performance logging and background diagnostics
Performance Logging (Developer settings) logs a resource sample every 5s
in the foreground and background: CPU by area and thread, wakeups, GPU,
memory, disk, activity counters as rates (mpv property updates, player
time publishes, renders, saves, network, images, CloudKit events, log
entries, main run-loop wakeups), SwiftUI body evaluations of the player
views, and app/player/battery context. Each foreground/background
transition logs a summary of the period. MetricKit metrics and
diagnostics (CPU exceptions, hangs) are logged whenever logging is on.

Background diagnostics (iOS) log lifecycle, audio session and power
events with an app/player/render snapshot and a heartbeat that detects
suspension gaps and main-thread stalls. Logs are persisted per session
in Caches/Logs and the previous session is included in exports, so logs
survive the app being swiped away.
2026-10-24 19:36:51 +02:00
Arkadiusz Fal
21a6238a16 Stop re-rendering hidden player UI during background playback
Every mpv time-pos tick updated PlayerState.currentTime and bufferedTime,
re-rendering the invisible player controls, progress bar and chapter
segments several times per second while the app played in the
background, which drained the battery during long audio-only sessions.

- PlayerState keeps currentTime/bufferedTime current for readers but
  stops notifying observers while publishesTimeUpdates is off;
  PlayerService turns it off on entering the background and back on
  (with one catch-up notification) before returning to the foreground
- MarqueeText pauses its animation timeline outside the active scene
- Replace the 100 ms polling loops in RemoteControlCoordinator and
  ToastManager (iOS/tvOS) with observation-driven waits

Main-thread samples during 30 s of background audio-only playback on an
iPhone 12 mini went from 1289 to 248.
2026-10-24 17:32:34 +02:00
Arkadiusz Fal
2a49a5db68 Use production CloudKit environment for Xcode builds
Development-signed builds now opt into the Production CloudKit
environment via com.apple.developer.icloud-container-environment,
driven by a CLOUDKIT_ENV build setting. A new Debug-CKDev configuration
and "Yattee (CloudKit Dev)" scheme switch to the Development
environment for schema work.

The DEV badge and banner in iCloud settings now reflect the actual
environment (YatteeCloudKitEnvironment Info.plist key) instead of
#if DEBUG, and the sync engine logs the environment at startup.
2026-10-23 21:47:34 +02:00
Arkadiusz Fal
c60f483d3c Raise Open Link URL limit from 20 to 200 2026-10-23 21:23:23 +02:00
Arkadiusz Fal
fb4da41877 Fix player brightness controls on iPhone Duo
Brightness controls used UIScreen.main, which on iPhone Duo can be a
different display than the one hosting the player. Route all brightness
reads and writes through PlayerBrightness, which targets the screen of
the window hosting the controls.
2026-10-22 23:16:11 +02:00
Arkadiusz Fal
1c57a55fe1 Return to Home when the selected sidebar source is removed 2026-10-22 07:58:09 +02:00
Arkadiusz Fal
b058474d31 Skip the queue sheet when the current video has finished
Tapping a video after the current one played to the end asked Play Now or Play Next, though replacing a finished video loses nothing. An ended video no longer counts as active playback; a non-empty queue still shows the sheet.
2026-10-21 22:16:23 +02:00
Arkadiusz Fal
582aa56644 Add keyboard shortcuts to toggle corner layout panels
Playback menu items toggle the info panel below the video (⌥⌘I), the
side column (⌥⌘S) and both at once (⌥⌘P) while the corner layout is
active. Showing one region from the hidden state brings back only that
region; hiding the last visible one hides the whole panel.
2026-10-21 22:06:27 +02:00
Arkadiusz Fal
582984f909 Fill the whole page with the opaque background on macOS
Content-sized pages like the Search empty state only got the opaque
window background behind their own frame, leaving darker window
material on both sides.
2026-10-21 20:36:47 +02:00
Arkadiusz Fal
e276c99d0b Allow pinch-to-panscan on the video in the corner layout
The window-level pinch was blocked whenever the landscape panel was
pinned and visible (the corner layout always counts as pinned) or a
stale portrait-panel flag was set. In the corner layout the pinch now
starts when it begins inside the video region, and the panscan
control button is enabled there too.
2026-10-20 23:43:47 +02:00
Arkadiusz Fal
14912f0203 Configure comments from their row in details panel settings
The separate comments group (pill mode, author avatars) is gone: comments
are a row in the pages/sections list whose editor holds both options.
Inline-sections hosts list comments as a locked pill row at the bottom;
a foldable iPhone in the corner layout lists real pages, including
comments and queue in the side column.
2026-10-20 22:26:21 +02:00
Arkadiusz Fal
cf880ab84b Add Copy Link to the macOS video context menu (#972)
SwiftUI's ShareLink menu on macOS offers no Copy Link item, so there was
no way to copy a video URL from a context menu. Add a macOS-only Copy
Link action next to Share.
2026-10-20 19:10:20 +02:00
Arkadiusz Fal
c4272c86f7 Start playlist Play at a random video in shuffle mode (#988)
Play always started from the first video, so shuffle only affected the
rest of the session. When the queue mode is Shuffle, rotate the list to
a random start so the remaining videos stay queued.
2026-10-19 23:26:41 +02:00
Arkadiusz Fal
ef372b65d9 Keep Settings in the tvOS sidebar (#986)
tvOS has no other route to Settings, so unchecking it in Sidebar settings
locked users out. Treat Settings as a required sidebar item on tvOS; an
already-stored hidden value is ignored because required items bypass the
visibility map.
2026-10-19 21:43:46 +02:00
Arkadiusz Fal
827d10eda7 Focus the delayed tvOS Cancel when it appears
By the time the button shows, focus has already been lost, and the
focus engine is not guaranteed to pick up an element inserted later.
2026-10-18 23:30:22 +02:00
Arkadiusz Fal
22ec48af3d Clear a stopped-load error when the playlist arrives anyway
A remote playlist load cancelled on tvOS can still land. If the playlist
it brings is empty, playlistBody showed the stale "Loading was stopped."
instead of the empty state.
2026-10-18 23:23:06 +02:00
Arkadiusz Fal
c19be0a0ef Give the empty tvOS import screens a focusable Done button
With nothing to import, the playlists and subscriptions import screens
had no focusable content; only the container's toolbar Done, which is
not a dependable focus target on tvOS.
2026-10-18 23:15:16 +02:00
Arkadiusz Fal
88551a1366 Offer a delayed Cancel while a tvOS remote playlist loads
A remote playlist this account does not own showed only a spinner while
loading, with or without a cached header, so the tvOS remote had nothing
to focus. Cancel stops at the error view and its Retry, since a sidebar
playlist is a tab root that cannot be popped.
2026-10-18 19:51:09 +02:00
Arkadiusz Fal
270e5ffdd3 Offer a delayed Cancel while a tvOS channel loads, and fill the blank state
Without a cached header the channel showed only a spinner, and when the
channel request failed while the videos request succeeded (no cache to
build the channel from) it rendered nothing at all; both left the tvOS
remote with nothing to focus. Cancel stops at the error view and its
Retry, since a sidebar channel is a tab root that cannot be popped; the
blank state now shows the load error with Retry on every platform.
2026-10-18 09:04:48 +02:00
Arkadiusz Fal
dd6bbbe691 Offer a delayed Cancel while the tvOS video page loads
The video page showed only a spinner until the video loaded, and
rendered nothing at all in the frame before a videoID load started;
either left the tvOS remote with nothing to focus. Both now show the
spinner with the delayed Cancel, which pops the page.
2026-10-17 23:49:13 +02:00
Arkadiusz Fal
70cda3fa45 Offer a delayed Cancel while tvOS external video extraction loads
A screen showing only a spinner has nothing focusable, so a slow or hung
extraction stranded the tvOS remote. Add TVStalledLoadCancelButton, a
Cancel that appears under a loading spinner after three seconds so
normal loads never flash it, and use it for external video extraction.
2026-10-17 21:57:56 +02:00
Arkadiusz Fal
b613b8dbc8 Keep the tvOS media browser header visible while loading
The Refresh and View Options header was hidden until the first listing
arrived, leaving only a spinner; an SMB or WebDAV connection can hang
for a long time with nothing the remote could focus. Refresh ignores
presses while a load is in flight.
2026-10-17 20:22:32 +02:00
Arkadiusz Fal
f393d2b9cc Give the tvOS network share discovery sheet a focusable Cancel
The toolbar Cancel is iOS/macOS only, so while scanning with no shares
found yet (and when discovery is unavailable) the tvOS sheet had nothing
the remote could focus.
2026-10-17 15:23:00 +02:00
Arkadiusz Fal
eec39445b4 Give the empty tvOS translators screen a focusable Done button
If the bundled translator list fails to load the screen is a bare
ContentUnavailableView, leaving the tvOS remote nothing to focus.
2026-10-17 08:59:14 +02:00
Arkadiusz Fal
34cec578ad Show an empty contributors list as a placeholder with Retry
A successful load with no contributors, and the first frame before the
load started, rendered an empty form with nothing focusable, stranding
the tvOS remote.
2026-10-17 07:01:21 +02:00
Arkadiusz Fal
4fe7cceda5 Give the empty tvOS PeerTube instance browser a Retry button
When the directory returned no instances without an error, the tvOS
search header was hidden and the no-results state had nothing focusable.
2026-10-16 23:59:30 +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
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