Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
58b767d
test(verify): shoot the player states for the v3 pixel pass
chhoumann Oct 13, 2026
2a86891
test(verify): wait for PodNotes' unload save before seeding
chhoumann Oct 13, 2026
f928134
fix(player): keep quarter playback rates and share the skip length rule
chhoumann Oct 13, 2026
88c1cc3
refactor(ui): make the Queue page's list a shared QueueList
chhoumann Oct 13, 2026
bede929
refactor(ui): give menus their trigger, a rate menu and a note entry
chhoumann Oct 13, 2026
2804fab
fix(ui): show seconds when less than a minute is left
chhoumann Oct 13, 2026
11bc4bc
feat(ui): replace the legacy player with the v3 now playing screen
chhoumann Oct 13, 2026
7a15c6a
Merge branch 'feat/ui-player' into ui2-impl
chhoumann Oct 13, 2026
2b1672f
feat(capture): show every capture in a Notice with an Undo that remov…
chhoumann Oct 13, 2026
9bbe486
test(player): keep quarter and one-decimal playback rates through the…
chhoumann Oct 13, 2026
6ff29dd
refactor(ui): fold the current chapter label's rule into the podcast …
chhoumann Oct 13, 2026
b308d11
fix(ui): keep focus on a menu's control when Escape closes the menu
chhoumann Oct 13, 2026
5f95a1e
test(e2e): drive capture with Undo, the video stage, Up next writes, …
chhoumann Oct 13, 2026
9e424fb
docs(verify): describe the capture Notice and its Undo
chhoumann Oct 13, 2026
3fd17fe
Merge remote-tracking branch 'origin/master' into feat/ui-player
chhoumann Oct 13, 2026
3cf3e1f
fix(ui): left-align chapter rows and keep Undo notices to two lines
chhoumann Oct 13, 2026
b228376
test(verify): keep the capture notice in the capture shots
chhoumann Oct 13, 2026
994eb48
fix(ui): refetch chapters after a failed load and keep a Chapters cho…
chhoumann Oct 13, 2026
709e7c8
fix(capture): offer no Undo for a capture that ends after the plugin …
chhoumann Oct 13, 2026
bf8fed2
fix(ui): give the video frame its own play and pause, and show a medi…
chhoumann Oct 13, 2026
54fb835
fix(ui): name the Timestamp button by its text and link the player's …
chhoumann Oct 13, 2026
90ccedc
test(verify): retry a test pattern whose ffmpeg run failed
chhoumann Oct 13, 2026
3291dd7
test(e2e): drive Space, a refused Undo and the podcast link in real O…
chhoumann Oct 13, 2026
755850d
test(e2e): read the Undo notice's message and the new player's elapse…
chhoumann Oct 13, 2026
5bb65d5
refactor: drop doc comments that restate their function's name
chhoumann Oct 13, 2026
aa6ab60
fix(ui): keep the latest episode's chapters and switch panels off dur…
chhoumann Oct 13, 2026
e201c4d
fix(ui): keep every episode's loaded chapters and keep panel controls…
chhoumann Oct 14, 2026
df49aed
Merge remote-tracking branch 'origin/master' into feat/ui-player
chhoumann Oct 14, 2026
ce78e39
fix(capture): bind Undo to where the capture put its text
chhoumann Oct 14, 2026
8bef8e5
test(e2e): drive an older Undo, Undo after the tab closed, and the tr…
chhoumann Oct 14, 2026
4546771
fix(capture): undo only while the note is exactly as the capture left it
chhoumann Oct 14, 2026
4d55bba
test: refuse Undo after a pasted copy and while a closing tab's save …
chhoumann Oct 14, 2026
d11513d
test: refuse Undo when the note changed on disk before Obsidian's cac…
chhoumann Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions .claude/skills/verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,31 +231,44 @@ its profile. It keeps the vault and `.obsidian-e2e-artifacts/`. Delete
`obsidian dev:cdp`. Its verbs are `click <css> [--text <s>] [--right]`,
`type <css> <text>` (insert at the cursor), `fill <css> <text>` (replace),
`key <Enter|Escape|Tab|ArrowUp|ArrowDown|Backspace|<char>|Ctrl+<char>>`,
`wait <css> [--text <s>]`, and `uri <obsidian://...>`. `click`, `type`,
`fill` and `wait` wait up to 10 s; `--timeout <ms>` changes that. They also
`wait <css> [--text <s>]`, and `uri <obsidian://...>`. `--text` matches a
substring; with `--exact`, only an element whose visible text is exactly
`<s>`. `click`, `type`, `fill` and `wait` wait up to 10 s; `--timeout <ms>`
changes that. They also
wait until nothing covers the element's center, and fail with
`covered by <tag.class>` when it stays covered, as under the full player. `key` and
`uri` do not wait. Each CLI call fails after 15 s without an answer.
- `.claude/skills/verify/screens.mjs` captures pixels, not behavior. Its verbs
are `place <sidebar|pane|phone>` (fails unless the view measures 320 x 720,
820 x 680, or a 390 x 844 phone screen), `theme <light|dark>`,
`seed <empty|library|episodes|lists|local|offline>` (replaces `data.json`
while PodNotes is disabled; `local` and `offline` also write media files into
the vault), `go '<page JSON>'`, `offline <on|off>`, `shot <file.png>`, and
`set <state|all> <label> [--widths ...] [--themes ...]`. `set` writes
while PodNotes is disabled and deletes the episode notes; `local` and
`offline` also write media files into the vault), `go '<page JSON>'`,
`offline <on|off>`, `shot <file.png>`, and
`set <state|player|all> <label> [--widths ...] [--themes ...]`. `set` writes
`.obsidian-e2e-artifacts/screens/<label>/<state>-<width>-<theme>.png` and a
`<state>-sheet.png` with the design prototype's screen beside each shot, when
the design has one. The states are `first-run`, `add-podcast`, `library`,
`episodes`, `error-feed`, `queue`, `playlists`, `playlist`, `latest`,
`played`, `local-files`, `offline` and `status-bar`. `set` opens each page
by its page JSON, so a state works on any version of the view. `shot` hides
Obsidian's status bar, except in `status-bar`, which puts a `9 properties`
item in it over the mini player and has no phone shot, because Obsidian hides
the status bar on mobile.
`played`, `local-files`, `offline` and `status-bar`, and the player's
`now-playing`, `mini-player`, `capture`, `chapters`, `up-next`,
`up-next-empty`, `no-chapters`, `video`, `local-player` and `media-error`.
`set player` runs the player's states and `set all` every state; both go on
past a failed state and fail at the end. `set` opens each page by its page
JSON, so a state works on any version of the view. A player state plays an
episode until its media loads and pauses it at a fixed second.
`capture`, `chapters`, `up-next` and `up-next-empty` then press the full
player's `Timestamp`, `Chapters` or `Up next` button, and fail at once
without a button that reads exactly that, as on a full player before v3.
`shot` hides Obsidian's status bar, except in `status-bar`, which puts a
`9 properties` item in it over the mini player and has no phone shot, because
Obsidian hides the status bar on mobile.
- `.claude/skills/verify/fixtures.mjs` runs as `feed`, `feed-b`, `transcripts`,
`library`, `retitle`, `requests`, `audio [vault path]` or `stop`. `library`
prints the URL of a manifest of eight podcasts, one whose feed answers 404, and
one more feed to add. `retitle` renames the first feed's two episodes to
one more feed to add. Their episodes' media is silence as long as the feed
says. `#213` has chapters, and `Signal & Noise` has a 10 s video episode that
ffmpeg makes on its first request and one whose media answers 404. `retitle` renames the first feed's two episodes to
`Renamed Opening Show` and `Renamed Closing Show`, keeping their GUIDs, until
the server stops. `requests` prints the paths
PodNotes fetched from the server. The feed URL uses the host `podnotes.test`,
Expand Down
20 changes: 10 additions & 10 deletions .claude/skills/verify/features/player.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@

The PodNotes view lives in the right sidebar. A user opens it, picks an episode
from a podcast's episode list, and the full player opens over the view with
artwork, title, progress, skip buttons, and volume and rate sliders while the
audio plays. Collapsing it leaves the mini player at the bottom of every page.
artwork, title, a chaptered scrubber, a rate pill, skip and play buttons, a
volume popover, Timestamp and Create note, and the Chapters and Up next panels
while the audio plays. Collapsing it leaves the mini player at the bottom of every page.

## Sub-features

- `player-open` reveals the view from the ribbon, and opens the full player from the "Show player" command.
- `player-collapse` collapses the full player to the mini player from its chevron or Escape, and opens it again from the mini player, without pausing.
- `player-play` starts a feed episode from its episode list.
- `player-toggle` pauses and resumes from the artwork button and the "Toggle playback" command.
- `player-toggle` pauses and resumes from the play button, Space, and the "Toggle playback" command.
- `player-skip` moves back and forward by the configured skip lengths (15 s by default).
- `player-seek` jumps to a position from a click on the progress bar.
- `player-rate` changes the playback rate from the slider and the rate commands.
- `player-seek` jumps to a position from a click on the scrubber or its arrow keys.
- `player-rate` changes the playback rate from the rate pill's menu and the rate commands.

## How to get to it (user POV)

Expand All @@ -34,10 +35,10 @@ Preconditions:
- **Open from the palette.** Run `UI key Ctrl+p`, `UI type .prompt-input 'Show player'`, `UI click .suggestion-item --text 'Show player'`. The same view is revealed, with no second PodNotes leaf (`CLI eval code='app.workspace.getLeavesOfType("podcast_player_view").length'` is `=> 1`). With an episode loaded, the full player (`.podcast-view section.pn-player`) covers the view.
- **Play an episode.** On the Library (`UI click '.podcast-view .pn-head button[aria-label^="Back to"]'` until `h1.pn-head-title` reads `Library`), run `UI click '.podcast-view button.pn-gridcard[aria-label="PodNotes Fixture"]'` and `UI click '.podcast-view .pn-ep-main' --text 'Fixture Episode 1'`. The full player opens with the title `Fixture Episode 1`, and after a few seconds `api.isPlaying` is true and `api.currentTime` grows.
- **Collapse and open.** Run `UI click '.podcast-view [aria-label="Collapse player"]'` or `UI key Escape`. The mini player (`.podcast-view section.pn-mini`) shows the episode and `api.isPlaying` stays true. Run `UI click '.podcast-view .pn-mini-open'` to open the full player again.
- **Toggle.** Run `UI click '.podcast-view [aria-label="Toggle playback"]'`. `api.isPlaying` flips and `api.currentTime` stops advancing.
- **Skip.** Run `UI click '.podcast-view .controls-container > :last-child button'` (forward) or `'.podcast-view .controls-container > :first-child button'` (backward). `api.currentTime` moves by 15 s, clamped to the episode.
- **Seek.** Run `UI click '.podcast-view [aria-label="Seek within episode"]'`. It clicks the bar's center, so `api.currentTime` lands near 30 s of the 60 s fixture.
- **Rate.** Run `CLI command id=podnotes:increase-playback-rate`. `api.playbackRate` and the label in `.playbackrate-container` both increase.
- **Toggle.** Run `UI click '.podcast-view .pn-player button.pn-play'`. `api.isPlaying` flips and `api.currentTime` stops advancing.
- **Skip.** Run `UI click '.podcast-view [aria-label="Forward 15 seconds"]'` or `'.podcast-view [aria-label="Back 15 seconds"]'`. `api.currentTime` moves by 15 s, clamped to the episode. The labels name the configured lengths.
- **Seek.** Run `UI click '.podcast-view [role="slider"][aria-label="Seek"]'`. It clicks the scrubber's center, so `api.currentTime` lands near 30 s of the 60 s fixture.
- **Rate.** Run `CLI command id=podnotes:increase-playback-rate`. `api.playbackRate` and the rate pill (`.pn-player button.pn-rate`) both increase. Click the pill and a menu item such as `1.25×` for the menu's speeds.
- **Proof.** Run `npm run screenshot:e2e-obsidian -- .obsidian-e2e-artifacts/player/playing.png` while it plays, and record a toggle or skip with `npm run record:e2e-obsidian -- .obsidian-e2e-artifacts/player/flow.mp4 -- <driver>`. Read `jq '.playedEpisodes' <data.json>` for the stored position.

## Gotchas
Expand All @@ -47,5 +48,4 @@ Preconditions:
- After a restart the view opens on its last page, and the mini player shows the last episode paused at its saved position. Nothing loads or plays until you press play. A sync that lands before then updates the shown episode and position, unless you moved the position.
- A video plays in the main window only. With the view in a popout, the player shows the artwork and the video keeps playing hidden in the main window.
- With `Keep a queue of episodes you switch away from` on (the default), switching episodes queues the previous one, and it plays again when the current one ends.
- The skip buttons have no accessible name. Address them by position inside `.controls-container`.
- Headless Obsidian plays to a null audio sink. Prove playback through `api.currentTime` advancing, not by sound.
2 changes: 2 additions & 0 deletions .claude/skills/verify/features/timestamps-uris.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ moment.

- `ts-capture` inserts the formatted current time at the cursor ("Capture Timestamp").
- `ts-segment` inserts a linked range ending now ("Capture Last 10 Seconds", "Capture Last 20 Seconds").
- `ts-undo` confirms every capture, from the player, the mini player, a command or the headset, in a Notice: `Timestamp added to <note>`, or `Created <note>` when it created the episode note. Its Undo puts the note back as the capture found it, only while the note is still exactly as the capture left it; any later change, also a pending save, gets the Notice `The note isn't as the capture left it, so the timestamp stays`. A created note is kept.
- `ts-format` applies the `Capture timestamp format` and `Timestamp offset (s)` settings.
- `uri-open` reopens the linked episode at `time`, also after another episode played.
- `uri-segment` plays a linked range and stops at its end (`endTime`).
Expand All @@ -31,6 +32,7 @@ Preconditions:
- **Set a linked format.** Open Settings > PodNotes ([feeds.md](./feeds.md)), run `UI fill '.modal.mod-settings textarea[placeholder="- {{linktime}} "]' '* {{linktime}} - '`, then close Settings. `data.json` `timestamp.template` is `* {{linktime}} - `.
- **Play.** Start `Fixture Episode 1` ([player.md](./player.md)) and wait about 5 s.
- **Capture.** Run `UI click '.workspace-leaf-content[data-type="markdown"] .cm-content .cm-line:last-child'`, `UI key Ctrl+p`, `UI type .prompt-input 'Capture Timestamp'`, `UI click .suggestion-item --text 'Capture Timestamp'`. After about 2 s, `CLI read path='Verify/Timestamps.md'` has a line like `* [00:00:05](obsidian://podnotes?episodeName=Fixture%20Episode%201&url=...&time=5.73) - `.
- **Undo.** Within 7 s of a capture, run `UI click '.notice button.pn-undo'`. The editor, then the file about 2 s later, reads as it did before the capture. Type into the captured line first, and Undo leaves it and shows the refusal Notice.
- **Segment.** Run the same palette steps with `Capture Last 10 Seconds`. The line holds a range such as `00:00:05-00:00:15` and its URI has `time` and `endTime`.
- **Open the link.** Let playback move on, run `CLI command id=markdown:toggle-preview` (Reading view), then `UI click '.markdown-reading-view a' --text '00:00:05'`. `api.podcast.title` is `Fixture Episode 1` and `api.currentTime` restarts from about 5.7 s. On Linux, the instance directory's `obsidian.log` (the parent of the private `HOME`) shows `Received callback URL` and `Processed URI`.
- **Direct URI path.** Run `UI uri 'obsidian://podnotes?episodeName=Fixture%20Episode%201&url=<encoded feed URL>&time=30'`. `api.currentTime` jumps to about 30 s.
Expand Down
Loading
Loading