Skip to content

Release/2.6 - #160

Draft
faultables wants to merge 34 commits into
developfrom
release/2.6
Draft

faultables wants to merge 34 commits into
developfrom
release/2.6

Conversation

@faultables

Copy link
Copy Markdown
Member

No description provided.

…branch

Staging: release/2.6 branched from develop (post 2.5 merge). MARKETING_VERSION 2.5 → 2.6 and build 304 → 305 on the flo target's Debug/Release configs, matching the release/2.5 bump convention (872aebb).
Hide the purple glyph layer in dark appearance (Icon Composer) so dark
mode shows the white glyph on the auto-dark background. Also refresh the
classic AppIcon dark PNG to match.
…uild

The mac fetch_and_increment_build_number queried latest_testflight_build_number
without a platform (default ios) while building the macOS train, and read the
repo number via agvtool (which returns the watch target's 211, not the flo
target's 305). On release/2.6 that computed max(ios 213, 211)+1 = 214 for the
osx train already at 320 → 409 CFBundleVersion must be higher than 320.

Query platform osx in the mac lane, and read the repo number as the max
CURRENT_PROJECT_VERSION over all configs via Xcodeproj in both lanes.
The version-scoped osx query finds nothing for a fresh 2.6 while Apple
enforces macOS build numbers across versions (osx 2.5 already at 320),
so the guard fell back to the repo number and built 306 → 409 must be
higher than 320. Drop the version filter in the mac lane so next is
max(any osx build, repo)+1 = 321.
- Window title follows the active tab (Scene title + visible titlebar);
  blank the duplicate in-toolbar page headings on Catalyst.
- Scope the Catalyst 900x500 min-size clamp to UINS* windows only: the
  global resize observer forced it onto NSPopupMenuWindow/popovers,
  blowing every Menu and context menu up to 900x500 (issue #158).
- Toolbar ellipsis menus use native button style + fixed icon-only labels;
  song context menus drop the VStack wrapper and use native Labels.
- Floating player: no drop shadow on Catalyst; AirPlay icon turns blue
  when a route is connected instead of showing the device name.
- Albums grid: 5 columns on Catalyst.
- Sidebar bottom bar: logged-in username (last-known/Offline fallback)
  with a Logout menu.
- Search: Catalyst hosts a custom field in the window toolbar (title
  area) via catalystAwareSearch; other platforms keep the drawer.
  AlbumsView inline field is iOS-only now.
- Preferences: hide the App Icon section when alternate icons are
  unsupported instead of showing a notice.
…ranch

navigationTitle(_:displayMode:) does not exist; the older navigationBarTitle
spelling with .automatic renders identically. (Local Catalyst builds never
compiled the iOS branch, so this slipped through.)
- Draw the page heading in the navigation bar (leading, system font,
  primary colour) and hide the titlebar title, so it shares a row with the
  search field and toolbar actions instead of sitting on a second row.
- Opt the search field out of the macOS 26 toolbar shared background so it
  reads as a separate control next to the filter menu.
- Lift the content column's navigation stack so the header sits 8pt below
  the window top instead of under a ~41pt empty titlebar strip. The SwiftUI
  host view is lifted rather than the navigation view, so SwiftUI
  re-inserting the navigation view on page changes can't drop the header
  back to the unlifted position.
- Add Catalyst-only headings for Home and Library (v2).
The floating player flashed during playback and popped out/in on every
tab switch in the macOS/iPad sidebar layout. Two compounding defects:

- The bar lived in an overlay inside every tab's content (sidebarTabContent),
  so TabView tore it down and re-created it with each tab's content and kept
  duplicates across the tabs it keeps mounted. Mount it once beside
  rootTabView, outside the TabView, keeping its content-column centering via
  the sidebar and side-panel insets.
- The 1s playback tick persisted nowPlayingProgress to UserDefaults on every
  tick. Every @AppStorage in the app is backed by UserDefaults, so ContentView
  re-ran every second while playing and UIKit rebuilt its tab bar items (the
  FLO-36 _tabs_rebuildTabBarItemsAnimated: path) — re-laying out the subtree
  the bar lived in, once per second. Persist progress on a 5s throttle and
  flush it on pause/seek instead.
FLO-36: let users pin library albums, artists and playlists for quick
access. Pins persist in UserDefaults (with one-time migration from the
previous downloads-only format) and surface in the Catalyst/iPad
sidebar Pinned section, the Library Pinned rows, and pin-first
download grids. Adds LibraryDestination.playlist so pinned playlists
deep-link through every navigation stack, and integrates pinned tabs
with the existing tab-selection clamp.

285 tests, 0 failures.
- Pinned sidebar tabs, Library rows and v2 cards now show real cover
  art (new PinArtworkView handles remote/local/fallback) instead of
  kind icons; sidebar uses a custom Tab label.
- Downloads Pinned/All-downloads headers: more horizontal spacing,
  pin glyph removed.
- Pin menus use specific titles (Pin album/artist/playlist).
PinArtworkView no longer renders a tinted icon tile when art is
missing/loading; it shows the app placeholder like every other
artwork surface.
- Pin/Unpin context menus now on v2 recently played, recently
  added, albums, songs and liked-songs rails (song rows offer
  Pin album for the song's album).
- v2 pinned cards grow to album size (148pt).
- Catalyst sidebar pinned tabs back to icon + title, no cover.
- one-row control bar: round Play/Shuffle (icon-only) plus inline
  Play Artist Radio / Play Top Songs pills
- ArtistDetailViewModel.fetchArtistSongs aggregates per-album tracks
- pinned artist/album tabs wrapped in NavigationStack so inner
  album/artist links navigate
- Catalyst pinned tabs show the item title as nav heading; window
  title resolves via displayName
Bake FLO_CHANNEL at build time (fastlane mac lane stamps beta/preview/store)
and render it as plain text next to flo in the Catalyst sidebar header.
…ned subtitle, wider player + now-playing menu

- Center pad floating player over content column with realistic
  sidebar widths (320 iPad / 260 Catalyst)
- Albums tab: single search field (native .searchable on iOS,
  custom toolbar field on Catalyst only)
- Right panel flush against content (gutter 0); seam is the panel's
  own divider so top-bar hover shows no empty strip
- Library v2 Pinned section subtitle: You pinned it!
- Catalyst floating player maxWidth 860 -> 1080
- Floating player now-playing ... menu left of lyrics:
  Go to Album, Go to Artist, Pin/Unpin album
Play Artist Radio / Play Top Songs swapped the whole label for a
spinner, shifting layout. Now only the leading icon swaps (fixed
20x20 slot) and the text stays put.
Presets picker (Off + 23 presets) in Preferences > Experimental.
Per-track audio tap applies 10 peaking biquads live; Off/Flat
bypasses with nil mix. Persists via UserDefaults.
Off/Flat attaches no tap, so Off->preset left the playing song
unequalized until the next track. Observe preset changes and
(de)attach the mix when bypass state flips; preset-to-preset
updates keep flowing through the tap's live gains.
- Persist queue order via new QueueEntity.position; snapshot/replace
  persistence in PlaybackService
- Play Next / Play After / Add to Queue for songs, albums and playlists
  (local-first resolution, offline-capable), wired into every song list
  and album/playlist grid context menu
- Queue rows: reorder (List onMove on iOS, drag-and-drop with
  commit-on-release previews on Catalyst/pad), swipe delete, Move to Top,
  Download/Remove Download, Like/Unlike, Go to Album/Artist, Clear Queue
- Unify phone sheet and pad/Catalyst sidebar on shared QueueSupport
  (QueueRowStore, QueueRowMenu, drop delegates)
Derive the sidebar inset from the live TabView detail width reported
by sidebarTabContent instead of a hardcoded 320pt estimate, so the
bar stays centered over the content column in portrait, landscape,
Stage Manager and collapsed-sidebar states.
Committing every DetailColumnWidthKey report straight into state
cascaded preference -> state -> full ContentView/TabView re-render on
every tick of a continuous window resize; the work per tick exceeded
the tick interval and the main thread never caught up (beachball until
the drag ends, panel open or not).

DetailWidthDebouncer stages reports without touching view state and
commits only after 200ms of layout quiet (plus 1pt epsilon), so resizes
stay at the baseline single-pass cost and the player recenters once the
layout settles.
nowPlaying force-subscripted the queue, so any reader evaluating in
the teardown window after the queue empties but before the presence
gates flip crashed with Index out of range. It now returns a blank
sentinel from a store-less scratch context (never saved/merged/
deleted) when the index is out of bounds; getAlbumCoverArt returns
early when empty and removeFromQueue guards a stale active index.
Adds QueueEmptyCrashTests covering remove-last, clear, and fresh-empty
reads.
…dit-mode reorder, output name above progress
…dismiss, top-aligned icons, artist play color, catalyst centering, iphone-only swipe-to-close
…max-reduce detail measurement + sidebar clamp
… 4px list inset, edit-gated menu, queue bottom bar, v1 ios search, neutral download button
…r, drag dim reset, v2 albums search, accent download button
…e cache stall

Session/ghost-login:
- Detect Subsonic HTTP 200 + status=failed (error code 40/41) as an auth
  failure in SubsonicEndpointRequest; non-auth failure codes (10, 20, ...)
  do not log out. Notify .sessionExpired from star/unstar and both download
  paths too (they previously bypassed ghost-session recovery).
- Re-verify the restored session on launch, on .networkBecameOnline and on
  app foreground, so a launch while the server was down no longer leaves a
  ghost isLoggedIn=true (verifyExistingSession, guarded against overlap and
  stale sessions).

Status dot / NetworkMonitor:
- HomeView dot is now a true tri-state: green only when logged in AND the
  server is reachable; orange when logged in but unreachable or logged out
  of a configured server; red on fresh install.
- Keep serverURL on logout so the dot can distinguish expired from fresh
  install and re-login is one tap with the URL prefilled.
- NetworkMonitor: generation-guarded probes fix the orphaned-timeout race
  (a stale probe can no longer overwrite a newer verdict), 30s periodic and
  app-active re-probes keep the flag fresh, offline transition cancels
  in-flight probes, and probeServerReachability(completion:) reports the
  verdict so callers stop reading a stale flag.
- ScrobbleQueueManager flushes only after the probe verdict lands (manual
  retry works first try when the server returns).

Cache stall:
- AlbumViewModel.refreshFailed flag: set on any failed load/refresh, cleared
  on success. LibraryView shows an orange 'Couldn't reach your server -
  showing cached library' banner with Retry in both v1 and v2; the cache is
  intentionally kept on offline refresh (stale beats empty). V2 refreshable
  chains deduped into refreshLibraryContent().

Catalyst sidebar:
- The bottom-bar menu always showed Logout regardless of login state and had
  no login entry point. It now offers Login... when logged out and
  'Logged in as <name>' + Logout when logged in.

Tests:
- Subsonic failed status 40 posts .sessionExpired; non-auth failure code
  does not; ND 401 regression guard; refreshFailed lifecycle.
Queue the next track behind the current one and let AVQueuePlayer advance
without tearing down the player item.

Gapless:
- primeGaplessNext() enqueues the next track at activation when it is
  already local (offline download or StreamCacheManager cache), so the
  transition needs no network.
- A cacheSong completion hook enqueues the local file the moment the
  pre-cache lands; otherwise the periodic observer enqueues the remote
  stream 10s before the end.
- currentItem KVO drives auto-advance state sync (index, progress,
  metadata, lyrics, scrobble-server, star); repeat-once queues a
  duplicate item, repeat-album wraps to index 0.
- Manual skips reuse the preloaded item via advanceToNextItem() and fall
  back to setNowPlaying() otherwise.
- Queue edits, reorder, shuffle and repeat-mode changes resync the
  preload; the replaceCurrentItem-era path remains the fallback when
  nothing is queued (end of queue, radio, preload failure).

Failure supervision:
- Observe the queued item's status; a failed preload is dropped and not
  re-armed for that index until the track/queue changes.
- A failed current item surfaces the error and auto-skips to the next
  distinct track, capped at 3 consecutive skips.
- DidPlayToEndTime with a queued item arms a 1.5s watchdog that falls
  back to a manual swap if AVQueuePlayer does not advance.
- setWillPlayNext() protects the preloaded file from cache eviction.
- Scrobble captures the track identity before the async submission so a
  very short track cannot scrobble the next one.
- EQ mix is refreshed on bind, since a pre-queued item may predate a
  preset change.

Tests: 9 new cases pin next-index and failure-skip resolution across
repeat modes. Docs updated in README and .wiki/features/audio-playback.md.
Crossfade (Preferences -> Experimental, Off by default):
- dual AVQueuePlayer design: the incoming track plays on a second player
  while the outgoing one fades out with an equal-power 20 Hz ramp over the
  actual time remaining, so the incoming track peaks exactly at the
  outgoing track's end.
- finishCrossfade promotes the incoming player to primary on ramp
  completion or on the outgoing item's DidPlayToEndTime, re-subscribing
  currentItem KVO and the periodic time observer and syncing now-playing,
  lyrics and scrobble state.
- pause, seek, skip, queue edits, clear-queue, and turning crossfade off
  cancel an in-flight fade; a failed incoming item cancels instead of
  fading into silence. The volume slider does not fight the ramp.
- Duration picker offers Off, 3, 5, 6, 8, 10 and 12 seconds; a positive
  duration enables crossfade and suppresses gapless preloading.

Gapless improvements:
- preloaded items now buffer with the system default instead of the 3 s
  stall-recovery cap; the cap is applied only once the item is current.
- when the stream cache completes after a remote preload was queued, swap
  it for the local file (remote -> local upgrade).
- refresh the EQ audio mix at bind only when the bypass state changed,
  instead of rebuilding a live MTAudioProcessingTap on every transition.
- transition diagnostics on Console.app (net.faultables.flo / player)
  gated behind the Debug setting: preload source, deadline arming,
  auto-advance, watchdog/fallback, and failure paths.

Tests: crossfade duration defaults to off, persists, and drives the
enabled state. 307 tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants