Skip to content

feat(ui): add the v3 player with Chapters, Up next and capture Undo - #466

Merged
chhoumann merged 33 commits into
masterfrom
feat/ui-player
Oct 14, 2026
Merged

chhoumann merged 33 commits into
masterfrom
feat/ui-player

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Why

The player was still the Svelte 4 player: a volume and a rate slider, the queue listed under it, and artwork you had to right-click to create a note. A capture gave feedback only from the mini player and could not be taken back. This PR builds the v3 player from the design spec and prototype and deletes what it replaces. Fixes #347 and #394.

What changed

  • Now playing. The head has Collapse and the episode menu. The stage shows the artwork, a file tile for a local file or artwork that fails to load (Local audio files show broken artwork in the player #394), a video frame with fullscreen and its own play and pause, or a media error with Retry in place of the artwork, no longer drawn over its icon. During a media error the transport is off. The deck has the title, a link to the podcast, a scrubber split at chapter starts, elapsed time, the current chapter and time left. Then come a rate menu (0.75 to 2), back and forward by your skip lengths, play and pause, and a volume popover. Last are Timestamp, the only accent button, which reads Added for a moment, and Create note or Open note (Add Capture timestamp and Episode note buttons to the player #347).
  • Chapters and Up next. At sidebar and phone widths a panel replaces the artwork. In a wide pane the panels are tabs in a right column. Chapters highlights and follows the current chapter, and a tap seeks and plays. Up next is the Queue page's own list, with the same drag and arrow-key reorder and one data.json write per move, and Edit opens the Queue page.
  • Capture feedback. Every capture, from the player, the mini player, a command, or the headset and lock screen, shows "Timestamp added to {note}" or "Created {note}" with Undo. Undo puts the note back as the capture found it, only while the note, in its open editor or else its saved file, is exactly as the capture left it. It works after the note's tab closed or switched to Reading view once that save has landed. Any other change, even text written after the capture, makes Undo leave the note and say so. The editor's own undo still covers a capture made in an open editor. Undo never deletes a note, and it stops working when PodNotes unloads.
  • Deleted. EpisodePlayer, PlayerOverlay, ChapterList, Loading, and the legacy Button, Icon, Slider, Progressbar and Image components. Every component is now a runes component.

Scope

  • Folded-in follow-ups: closing a menu returns focus to the view, so the next Escape reaches the player. The doubled volume and speed values went with the sliders. A downward grip drag on a phone no longer starts Obsidian's pull-down action. Time left under a minute shows seconds.
  • Left out: the Transcript panel (a separate decision), the settings tab (UI3), and arrow keys between the tabs and phone polish (UI4).

Deviations from the design

  1. Capture feedback is an Obsidian notice with Undo, not the in-view toast of SPEC 3 (capture) and SPEC 4 (Toast). It has no destination chip, no playable line and no headset chip. One feedback surface, already bound to the plugin's lifetime, fits the size budget.
  2. Escape closes a menu or modal, then the volume popover, then the player. A notice is not in Obsidian's keymap, so Escape leaves it.
  3. Undo of a capture that created the episode note removes the line and keeps the note. Undo refuses once the note changed in any way, including text written after the capture, and also while a closing tab's save is still pending.
  4. The panel switch has Chapters and Up next. The Transcript panel is not built.
  5. A local file names its folder as its source, as the prototype does. The video frame and its buttons use theme colors, not the prototype's black.
  6. Up next lists played episodes even with Hide played on, as the Queue page does.

API changes

  • The playbackRate setter of the public API and the saved default speed keep hundredths, so 1.25 stays 1.25 instead of becoming 1.3. The type and the data shape are unchanged. The rate menu's 0.75, 1.25 and 1.75 need it.

Blast Radius

Every user sees the new player. Captures from commands and the headset, silent until now, show a notice. Capture and Undo write only to the note that took the capture, and Undo refuses rather than guess. The player stays out of PlayerController's position and save logic.

Verification

  • lint, format:check, typecheck, build, test (svelte-check 0 errors and 0 warnings; 2,481 unit tests), test:tooling (13) and docs:build pass. Each new unit test and journey failed on a defect planted on purpose before it was trusted.
  • test:e2e passes 154 of 154 on a fresh vault in Obsidian 1.14.4 (Linux). New journeys cover capture from the full player and the mini player into the episode note and into an open note, each with Undo; Undo refused after a pasted copy of the line and after an edit; Undo after the note's tab closed; Undo refused while the closing tab's save is pending; capture with nothing loaded; a media error with Retry and the transport off; a video beside a 14-episode queue; Up next reorder with one data.json write per move; a local file's tile; Space; and the Escape order.
  • Two reviewers, claude-fable-5-1 and gpt-6-astra, reviewed the change in three rounds, and their accepted findings are fixed here. Among them were a chapters cache that kept a failed load, an Undo offered after unload, fullscreen video without a play control, and a media error hidden by an open panel. Independent verifiers (V-466, V-466b) then found Undo defects, fixed by one rule: Undo acts only while the whole note is exactly as the capture left it. The coordinator's ceiling for this unit is 98,950 gzip.

Size (gzip, npm run size):

Step gzip vs master
master 427c9d5 98,239
Quarter playback rates, shared skip length 98,241 +2
Shared QueueList 98,298 +59
Menus: focus, rate menu, note entry 98,347 +108
v3 player and every deletion 97,980 -259
Capture notice with Undo 98,396 +157
Tightening pass 98,386 +147
Menu Escape focus fix 98,419 +180
master f295af1 (PLAY2) merged 98,506 +182 vs 98,324
Chapter rows, two-line notices 98,566 +242
Review fixes, three rounds 98,859 +535 vs 98,324
master b14edf1 (#464) merged 98,871 +535 vs 98,336
Undo bound to where the capture went 98,943 +607 vs 98,336
Undo only while the note is as the capture left it 98,927 +591 vs 98,336

Screenshots from Obsidian 1.14.4. Each sheet has a row per player state, with master, this branch and the design prototype side by side. Capture, Chapters, Up next and an empty Up next have no master column, because master has no such state.

Sidebar 320, light and dark:
Sidebar light
Sidebar dark

Pane 820, light and dark:
Pane light
Pane dark

Phone 390 (emulated), light and dark:
Phone light
Phone dark

Capture into an open note, then Undo: screen recording
Capture and Undo, frame by frame

Known issue

While the full player is open, keys a note handles itself, such as Enter and Escape in its find bar, do not reach the note. The player's key scope has done this since the overlay player (#434). It is unreleased. The fix registers the player's keys on the view's own scope, and it belongs in its own PR.

Docs at release

  • podcasts.md, the player section: the rate menu (0.75 to 2), the volume popover, the back and forward buttons labeled with your skip lengths, Space to play and pause, the scrubber keys (Left and Right 5 s, Page Up and Page Down 30 s, Home and End), and the Escape order. The Chapters and Up next panels, as tabs in a wide pane. Create note and Open note beside Timestamp. Retry after a media error. The file tile for local files. Remove the volume and speed sliders, the Queue list under the player, and the queue move items that list had.
  • timestamps.md: every capture shows "Timestamp added to {note}" or "Created {note}" with Undo. Undo puts the note back only while the note is exactly as the capture left it, says so when it leaves the note, and keeps a created note. To take back a capture after writing more, use the editor's own undo. The commands and the headset button show the same notice. Timestamp reads Added for about a second.
  • api.md: the playbackRate setter keeps hundredths (1.25 stays 1.25).
  • docs/scripts/screenshots.mjs: the player changed, so regenerate its screenshots.

Note

Add v3 full player with Chapters, Up next panels and capture Undo

  • Adds a stateful full player (Player.svelte) rendered by PodcastView.svelte with a chapter-aware Scrubber, a Chapters panel that seeks and tracks the active chapter, and an Up next panel backed by the renamed QueueList.
  • Playback-rate handling now keeps hundredth precision (e.g. 1.25) in playbackRate.ts, and a new rate menu in menus.ts offers quarter steps from 0.75× to 2×. Menus restore focus to their opening control and consume Escape.
  • Timestamp captures now show a notice with an Undo action. CaptureService.ts snapshots the editor or episode-note file and restores it on Undo only when content still matches; changed notes get a refusal notice instead. The service is disposed on plugin unload.
  • Expands test infrastructure: browser-e2e player journeys, richer fake editor/vault/workspace fixtures, ffmpeg-generated video fixtures, chapter feeds, and player screenshot recipes.
  • Behavioral Change: formatMinutes now shows seconds for durations under 59.5s (min 1 s); MiniPlayer no longer shows its own success notice and uses sourceOf labels; Escape during queue drag now ends the drag via an Obsidian keymap scope.

Macroscope summarized d11513d.

screens.mjs gains ten player states: now-playing, mini-player, capture,
chapters, up-next, up-next-empty, no-chapters, video, local-player and
media-error, and `set player <label>` runs them. A player state plays the
scene's episode until its media loads, pauses it at a fixed second, saves
it there and opens the full player. capture, chapters and the two up-next
states press the full player's button by its exact text, so they fail at
once on a player without it. `set player` and `set all` go on past a
failed state and fail at the end. seed deletes episode notes, so a
capture cannot change a later state's shots.

fixtures.mjs serves each library episode as silence as long as its feed
says, streamed so an hour costs no memory. #213 carries the prototype's
eight JSON chapters, and Signal & Noise gains a 10 s H.264 test pattern
that ffmpeg makes on first request and an episode whose media answers 404.

ui.mjs gains --exact, so "Chapters" does not also match "Chapters (8)".
disablePlugin resolves before the save PodNotes starts on unload, and
that save merges its unsaved edits into data.json. seed wrote the next
scene in that window, so a late save could carry the previous state's
feeds or progress into it. seed now awaits flushSaves() on the disabled
instance, which resolves once the unload save is done.
normalizePlaybackRate rounded to tenths, so 0.75, 1.25 and 1.75 became
0.8, 1.3 and 1.8. It now rounds to hundredths; the rate commands still
step by 0.1. normalizeSkipLength moves from the API to src/utility so the
player can label its skip buttons with the same rule.

size: 98,241 gzip (+2 vs master 98,239)
QueuePage becomes QueueList with an empty label, so the player's Up next
panel can render the same keyed rows and the same one-write drag. A drag
now cancels on Escape through a scope it pushes for its lifetime: inside
the full player, the player's scope takes Escape before a window listener
could hear it. The grip carries data-ignore-swipe, so on a phone a
downward drag starts none of Obsidian's swipe gestures.

size: 98,298 gzip (+59 vs master 98,239)
A menu now knows the control that opened it. Focus goes back there before
a picked item runs, so an item that opens a note keeps the focus it sets,
or when the menu closes with nothing picked, so the next Escape reaches
the view. rateMenu offers 0.75 to 2 with the live rate checked, and
noteEntry is the episode menu's Open note or Create note, for the player's
capture bar to share. The Menu mock keeps its items and can pick or close.

size: 98,347 gzip (+108 vs master 98,239)
Rows, the mini player and list totals rounded anything under a minute up
to "1 min", so a nearly finished episode read "1 min left" with seconds
of audio remaining. Under a minute they now show seconds, such as
"45 s left".
The full player is one Player component: a head with collapse and the
episode menu; a stage with the artwork, a file tile for a local file or
artwork that fails to load (#394), the video with fullscreen, the media
error with Retry, or a panel at compact widths; and a deck with the
title, the podcast or folder link, a chaptered Scrubber, the current
chapter, a rate pill menu, skip buttons labeled with the configured
lengths, play and pause, a volume popover, Timestamp and Create or Open
note (#347), and the Chapters and Up next switch. Wide views put the
panels in a right column with tabs. Up next is the shared QueueList, so
it reorders in place with one write per move. Escape closes the volume
popover before the player, and Space plays and pauses off controls.

EpisodePlayer, PlayerOverlay, ChapterList, Loading, the obsidian Button,
Icon and Slider wrappers, Progressbar, Image and their helpers go. The
e2e targets and the verify skill's player notes follow the new markup.

size: 97,980 gzip (-259 vs master 98,239)
…es exactly what it inserted

CaptureService now confirms each successful capture, whatever started it (the
player, the mini player, the commands, the headset or lock screen), with an
Undo notice: "Timestamp added to {note}", or "Created {note}" when the
capture created the episode note.

Undo takes back exactly what the capture inserted, and only while it is
still exactly where it was put:
- in an editor, while the same tab still shows the same note in Source or
  Live Preview and the inserted range is untouched; text typed after it
  stays, and a selection the capture replaced comes back;
- in the episode note, while the note still ends with the appended text,
  through the note's open editor when one edits it, else through a
  vault.process that aborts when the end changed. A note the capture created
  is kept; only the timestamp goes.
When the text was edited since, Undo leaves it and says so. The Undo is
disarmed when the plugin unloads (dismissUndoNotices in onunload).

The full player's Timestamp button reads "Added" with a check for 1.2 s
after a capture. The player and mini player no longer show their own
"Timestamp added to" Notice.

size: 98,396 gzip (+157 vs master 98,239)
… API and the saved default

1.25 round-trips through the IAPI playbackRate setter and through the saved
default speed, and a 2.21.1 library's one-decimal default speed (1.3) loads
and saves unchanged with no normalization warning.

size: 98,396 gzip (+157 vs master 98,239)
…link's

The tightening pass over the new player measured three candidates against
98,396 gzip:
- folding ChaptersPanel into Player saves 93 bytes, under the 100-byte bar,
  so it stays a component;
- one shared reset for the player's bare buttons costs 3 bytes more than the
  duplicates it removes, so the per-group resets stay;
- the current chapter label's own rule (min-width 0, font-size inherit) merges
  into the podcast link rule it already shares, with no visual change.

size: 98,386 gzip (+147 vs master 98,239)
Obsidian's menu closes on Escape but leaves the key unhandled, and Obsidian
then sends focus from a sidebar view to the main area's most recent leaf.
With the player in the right sidebar and a note open, Escape on the episode
menu moved focus into the note, so the next Escape went to the note instead
of closing the volume slider or the player. The menu now marks the Escape
that closes it as handled, so focus stays on the control that opened it.

size: 98,419 gzip (+180 vs master 98,239)
…local artwork and the Escape order

In real Obsidian 1.14.4:
- J-TS-12: the full player's Timestamp goes into a Live Preview note at the
  caret, reads "Added", and Undo through the Notice leaves the note byte for
  byte as before; with no editor open it creates the episode note, Undo keeps
  the note without the line, and a second capture's Undo leaves it as before.
- J-TS-13: the same from the mini player, into a note and into an episode
  note the user has, whose missing final newline Undo takes back too.
- J-TS-14: Capture Timestamp with nothing loaded shows "Play an episode to
  capture" and writes nothing.
- A video episode with 30 queued keeps its 16:9 frame at 320 px, after Up
  next covered the stage, and at 820 px beside Up next.
- Up next reorders by a slow drag and by an arrow key, writing data.json
  once per completed move.
- #394: a local file shows its file tile, never an image, in the player and
  the mini player.
- With a note open in the main area, Escape closes the episode menu, then
  the volume slider, then the player, and focus stays in the view.

size: 98,419 gzip (+180 vs master 98,239)
size: 98,419 gzip (+180 vs master 98,239)
Obsidian centers a button's content, so chapter rows centered their time
and title instead of starting each at the left as the design shows. A long
note or podcast name made an Undo notice four or five lines tall; it now
stops at two, with Undo beside it.

size: 98,566 gzip (+242 vs master f295af1 98,324)
Capture feedback is an Obsidian notice, so the capture state keeps notices
in its shots, fails unless one says "Timestamp added to", and clears older
notices before it presses Timestamp.
…ice with its episode

A chapters fetch that failed, as offline, was memoized as an empty list, so
Chapters stayed hidden for the session. Only loaded chapters are kept now, and
a reopened player takes them on its first frame instead of showing Up next
until a promise settles. A panel choice resets when the chapters change, so a
Chapters choice no longer comes back on its own on a later episode.

size: 98,605 gzip (+281 vs master f295af1 98,324)
…unloaded

CaptureService awaits the note write before it offers Undo, so a capture
still running when the plugin unloaded showed an armed Undo after
dismissUndoNotices had run. onunload now disposes the service, and a
disposed service offers no Undo.

size: 98,623 gzip (+299 vs master f295af1 98,324)
…a error over an open panel

Fullscreen shows only the video frame, whose one control was Fullscreen. The
frame now carries a play/pause button over the video, showing its icon while
paused or under the pointer, as the replaced player's overlay did. A media
error now takes the stage before a compact panel, so its message and Retry
show when Up next or Chapters was open.

size: 98,770 gzip (+446 vs master f295af1 98,324)
…tabs to their panel

The Timestamp button's accessible name now follows what it shows, Timestamp
and then Added. The wide player's tabs carry ids and aria-controls, and the
tab panel is labelled by the selected tab. The scrubber shows an empty track,
not width NaN%, before the duration is known, and the menu's Escape comment
says why it holds on desktop and does nothing on a phone.

size: 98,825 gzip (+501 vs master f295af1 98,324)
A failed run stayed cached as a rejected promise, so every later request for
the video answered 500 until the fixture server restarted.

size: 98,825 gzip (+501 vs master f295af1 98,324)
…bsidian

The player journey presses Space with the focus on the player's title and
checks that api.isPlaying flips both ways, then opens the podcast's page from
the link under the title. J-TS-15 writes a line after a capture into the
episode note and checks that Undo leaves both lines and says so. A unit test
covers the link for a saved and an unsaved podcast.

size: 98,825 gzip (+501 vs master f295af1 98,324)
…d time

The Undo notice now wraps its message in a span, so dismissNotice found no
text to click and J-FEED-05 and the shared-feed Remove journey timed out.
The synced-progress journeys from master read the replaced player's
.episode-player status line; they read the new player's elapsed time now.

size: 98,825 gzip (+501 vs master f295af1 98,324)
…ing a media error

A slower chapters load for an earlier episode could replace the cached
chapters of the episode on screen, so reopening the player fetched again and
showed Up next first. At compact widths a media error takes the stage, so the
panel switches now read off and are disabled until the media loads.

size: 98,854 gzip (+530 vs master f295af1 98,324)
… off during a media error

A one-entry chapters cache let a slower or earlier load evict the chapters
of the episode on screen, also after a return to that episode. Chapters now
stay per address for the session, so no load order can evict them. During a
media error at compact widths the current chapter label no longer opens a
panel, and disabled switches no longer take the hover style.

size: 98,859 gzip (+535 vs master f295af1 98,324)
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: d11513d
Status: ✅  Deploy successful!
Preview URL: https://25d35bb3.podnotes.pages.dev
Branch Preview URL: https://feat-ui-player.podnotes.pages.dev

View logs

Undo found an episode note capture by the note's ending, so after the user
wrote the same line again, an older Undo removed the later line and kept
its own. Undo also refused once the note's tab closed or switched to
Reading view, though the note had not changed.

Each capture now keeps where its text went: the offset, the text and the
selection it replaced. Undo takes the text out of an editor open on the
note, else out of the saved file in one vault.process, and only while that
exact range still holds exactly the inserted text. Text written after the
capture stays. Otherwise Undo leaves the note and says so.

size: 98,943 gzip (+607 vs master b14edf1 98,336)
Matching the inserted text at its old offset was not identity: a copy of
the capture pasted before it sat at that offset, so Undo deleted the copy.
Undo also refused while a closing tab's save was still pending, with a
message that said the note had changed.

A capture now keeps the note's text before and after it. Undo restores the
text before only while the note, in its open editor or else its saved file,
equals the text after. Any other state refuses, including an edit anywhere
and a save still pending, with a message that holds in every case. In an
editor, Undo replaces only the changed stretch, so the caret and the
editor's own undo history stay.

size: 98,927 gzip (+591 vs master b14edf1 98,336)
…he saw it

The unit vault fake now has cachedRead, read, modify and process, with a
cache that a change on disk does not update, as Obsidian's. A unit test and
J-TS-19 change the episode note behind that cache and click Undo: it must
refuse and keep the newer text. Both fail when Undo reads with cachedRead
and writes with modify instead of one vault.process.
@chhoumann
chhoumann merged commit 35461cd into master Oct 14, 2026
16 checks passed
@chhoumann
chhoumann deleted the feat/ui-player branch October 14, 2026 04:10
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.

Add Capture timestamp and Episode note buttons to the player

1 participant