Extract currentTime/duration/isPlaying into a separate PlaybackProgress
ObservableObject so the 0.5s time-observer ticks only invalidate the seek
bar and play-pause button subviews, not the entire FullPlayerView or
MiniPlayerView. Menus no longer blink or lose their checkmarks during
playback.
Also fix chapter list selection in FullPlayerView to call audioPlayer.load()
directly instead of relying on skip notifications (which only worked when
ChapterReaderView was open).
Previously, prev/next buttons were relative to the currently playing
chapter and only worked for one navigation step. They relied on
audioPlayer.prevChapter and audioPlayer.nextChapter which were set
when loading audio and never updated.
Now buttons use absolute chapter navigation:
- Previous: always navigate to (current chapter - 1)
- Next: always navigate to (current chapter + 1)
- Bounded by chapter 1 and max chapter number
- Works continuously without stopping after one navigation
Added AudioPlayerService.absolutePrevChapter and .absoluteNextChapter
computed properties that calculate the prev/next chapter numbers based
on the current chapter and total chapters list.
Updated both mini player and full player to use absolute navigation.
Change seek gesture from .gesture to .highPriorityGesture to ensure
horizontal press-and-drag seeking takes precedence over the vertical
swipe gesture. This fixes unreliable seeking when dragging on the
mini player progress bar.
- Moved audio start button from top-right toolbar to floating bottom-right position
- Only shows floating button when audio player is not active
- Button styled as amber pill with 'Listen' text and play icon
- More accessible placement for quick audio playback start
- Positioned 20pt from bottom and right edges with shadow
- Created 1024x1024 app icon with amber/orange gradient background
- Added white open book icon in center with pages and spine
- Gradient matches app's amber accent color theme
- Clean, minimal, modern design suitable for iOS
- Added previous chapter button (matching next button style)
- Made progress bar interactive - now occupies full background of mini player
- Horizontal drag/tap on progress bar seeks audio (shows amber overlay while seeking)
- Progress bar now uses full pill shape as background instead of small bottom border
- Tightened button spacing (12pt instead of 16pt) to fit prev/next buttons
- Changed vertical drag to simultaneousGesture to not conflict with horizontal seek
- Buttons slightly smaller (40pt) to accommodate prev button in layout
- Reduced bottom padding from 24pt to 8pt on bottom toolbar
- Reduced spacer above toolbar from 24pt to 16pt
- Added .ignoresSafeArea(edges: .bottom) to VStack to extend to screen bottom
- Player now uses full screen height without excess bottom padding
- Added custom init to ChaptersListSheet that sets scrollPosition state before view appears
- Removed onAppear scroll position update (now set in init)
- List renders directly at current chapter position without any scroll animation
- Better performance for books with 1000+ chapters (no initial render + animate delay)
- Changed progress bar from RoundedRectangle to Capsule for rounded ends
- Added horizontal padding so indicator wraps nicely around bottom corners
- Progress bar now feels like an integrated part of the pill-shaped mini player
- Full player prev/next buttons now dismiss the player before navigating (allows user to see chapter change)
- Added amber loading indicator on next chapter button when prefetching audio (both mini and full player)
- Indicator appears as small ProgressView in bottom-right corner of forward button
- Fix chapter-based sleep timer not resetting when auto-advancing to next chapter (sleepTimerStartChapter now updates in handlePlaybackFinished)
- Fix AirPlay button being too large compared to other toolbar icons (constrain to 22×22)
Replace ScrollViewReader with scrollPosition to instantly show the current chapter without visible scrolling. This scales better for books with 1000+ chapters.
AirPlay:
- Replaced placeholder AirPlay button with AVRoutePickerView
- Users can now cast audio to AirPlay devices
- Icon changes color when AirPlay is active
Sleep Timer:
- Added sleep timer sheet with multiple options
- Chapter-based: 1, 2, 3, or 4 chapters
- Time-based: 20 mins, 40 mins, 1 hour, 2 hours
- Moon icon fills when timer is active and turns amber
- Timer stops playback when target is reached
- Chapter-based timer tracks chapters played from start
- Time-based timer uses async task with proper cancellation
- Timer is cancelled when playback stops manually
- SleepTimerOption enum supports both timer types
- List bullet button now opens a chapters list sheet
- ChaptersListSheet displays all chapters with scroll view
- Current chapter highlighted with amber badge and background
- Auto-scrolls to current chapter when opened
- Chapter selection uses skip notifications to navigate
- Supports both .medium and .large presentation sizes
- Shows "Now Playing" label on current chapter
- Clean list design with chapter numbers and titles
- Fully pill-shaped design (cornerRadius 40) with dark glassmorphic background
- Larger cover thumbnail (56×56 with 12pt corner radius)
- Improved layout: cover + chapter/book titles + play/pause + next chapter skip
- Progress indicator moved to bottom 3pt amber border
- Removed dismiss X button - swipe down to dismiss instead
- Added swipe gestures: up to expand (0.4x resistance), down to dismiss (0.8x resistance)
- Dismiss animation with fade and scale effects
- Skip forward button navigates to next chapter (disabled when unavailable)
- Updated mini player height to 92pt in AppLayout
- Add safeAreaInset on ChapterReaderView ScrollView so Prev/Next buttons
clear the mini player bar when audio is active
- Fix mini player height reservation (66pt constant via AppLayout)
- Add AsyncCoverImage(isBackground:) to suppress rounded-rect placeholder
showing through blurred hero background (empty square bug)
- Reduce chapter page size 100→50 on iOS
- Fix ChapterRow date/chevron layout: minLength spacer + fixedSize date
- Add String.strippingTrailingDate() extension; apply in MiniPlayer,
FullPlayer, and ChapterReader header to clean up stale concatenated titles
- Make Prev/Next buttons equal-width (maxWidth: .infinity) for easier tapping
- Replace AudioGenerateResponse with AudioTriggerResponse to handle both 200 (cached) and 202 (async job) responses
- Add pollAudioStatus() in APIClient that polls /api/audio/status every 2s until done or failed
- Update generateAudio() and prefetchNext() in AudioPlayerService to poll instead of expecting a synchronous URL response
- Bump chapter list font sizes: number xs→sm, title sm→base, date/badge xs→sm, row padding py-2→py-2.5
New page mirrors the scrape tasks pattern: loads audio_jobs from
PocketBase via a new listAudioJobs() helper, shows a filterable table
(slug, chapter, voice, status, started, duration, error), and live-polls
every 3s while any job is pending or generating. Also fixes the
/admin/audio nav active-state check (was startsWith, now exact match)
to prevent it from matching /admin/audio-jobs.
- AudioPlayerService: replace loading/generating states with a single
.generating state; presign fast path before triggering TTS; fix URL
resolution for relative paths; cache cover artwork to avoid re-downloads;
fix duration KVO race (durationObserver); use toleranceBefore/After:zero
for accurate seeking; prefetch next chapter unconditionally (not just when
autoNext is on); handle auto-next internally on playback finish
- APIClient: fix URL construction (appendingPathComponent encodes slashes);
add verbose debug logging for all requests/responses/decoding errors;
fix sessions() to unwrap {sessions:[]} envelope; fix BrowseResponse
CodingKey hasNext → camelCase
- AudioGenerateResponse: update to synchronous {url, filename} shape
- Models: remove redundant AudioStatus enum; remove CodingKeys from
UserSettings (server now sends camelCase)
- Views: fix alert bindings (.constant → proper two-way Binding); add
error+retry UI to BrowseView; add pull-to-refresh to browse list;
fix ChapterReaderView to show error state and use dynamic WKWebView
height; fix HomeView HStack alignment; only treat audio as current
if the player isActive; suppress CancellationError from error UI
Replace blocking POST /api/audio with a non-blocking 202 flow: the Go
handler immediately enqueues a job in a new `audio_jobs` PocketBase
collection and returns {job_id, status}. A background goroutine runs
the actual Kokoro TTS work and updates job status (pending → generating
→ done/failed). A new GET /api/audio/status/{slug}/{n} endpoint lets
clients poll progress. The SvelteKit proxy and AudioPlayer.svelte are
updated to POST, then poll the status route every 2s until done.
- justfile: extract ios_scheme/ios_sim/ios_spm/runner_temp variables; add
ios-resolve (SPM dep cache), ios-archive, ios-export recipes; pipe xcodebuild
through xcpretty with raw fallback in ios-build/ios-test
- ios.yaml: replace raw xcodebuild calls with just ios-build / just ios-test;
install just via brew; uncommented archive job stub now calls just ios-archive
&& just ios-export; add justfile to path trigger
- ci-scraper.yaml: install just via extractions/setup-just@v2; use just lint /
just test; fix upload-artifact v3 -> v4; add justfile to path trigger
- ci-ui.yaml: install just; use just ui-install / just ui-check / just ui-build;
add justfile to path trigger
- Add user_sessions PocketBase collection (user_id, session_id, user_agent, ip, created/last_seen)
- Extend auth token format to include a per-login authSessionId (4th segment)
- Hook validates authSessionId against DB on each request; revoked sessions are cleared immediately
- Login/register create a session record capturing user-agent and IP
- Profile page shows all active sessions with current session highlighted; per-session End/Sign out buttons
- GET /api/sessions and DELETE /api/sessions/[id] endpoints for client-side revocation
- Backward compatible: legacy 3-segment tokens pass through without DB check