Skip to content

feat(ui): add the Queue, Playlists, Local files and list pages, with offline mode - #459

Merged
chhoumann merged 59 commits into
masterfrom
feat/ui-lists
Oct 13, 2026
Merged

chhoumann merged 59 commits into
masterfrom
feat/ui-lists

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Why

The v3 view still showed today's list pages: one filter bar on every list, a queue reordered from a modal, and playlists managed only in Settings. This PR builds the remaining list pages from the design spec and prototype and deletes the legacy UI they replace. It also fixes #453: Obsidian's status bar covered the mini player's play/pause.

What changed

  • Lists. Latest episodes, Played, a playlist (Favorites too) and Local files are list pages with the design's head: title, a count or time-left flair, and the page's action. At sidebar and phone widths the podcast name (a local file's folder) takes its own line; in the pane the row stays on one line. Local files show a file tile and their folder, and a plus button opens the vault media picker.
  • Queue. Rows have a grip, cover, title, podcast, time left and remove. Drag a grip, or press Up and Down on it, to reorder in place. A move places an episode before or after another episode by identity, never by index, so a queue that changes during a drag never moves the wrong episode. A drag shows its order as it goes and saves once, on release; Escape cancels it. The queue menu toggles "Queue episodes I switch away from" and clears the queue with Undo. The Reorder Queue command opens the Queue page; the modal is gone.
  • Playlists. Favorites first, then your playlists with count and total length, then New playlist (a prompt; an empty name keeps focus). A playlist's menu deletes it with Undo, and Add to playlist ends with New playlist. The Settings playlist manager is gone.
  • Podcast menu. Remove podcast offers Undo, which restores the exact saved record at its grid position, with a private feed's address, also after several removals. A pending Undo ends when PodNotes unloads. Copy feed URL appears for public feeds only, and Edit feed URL for a public feed that failed; a pasted private URL is stored as a secret, never in data.json.
  • Offline. One online signal (navigator.onLine and its events) drives an Offline strip, "Available offline" and "Needs a connection" groups, and a "Not downloaded" notice before playback starts. Refresh and transcribe are disabled, and podcast search answers "Search needs a connection" without a request.
  • Onboarding. Desktop now opens the view once, on a new install, like mobile, and then keeps the user's layout.

Scope

  • The player (UI2) and the rest of the Settings tab (UI3) are unchanged, apart from the Playlists section leaving Settings, the player's queue menu losing its move items, and the player keeping its last row clear of the status bar.
  • No data.json shape, public API or obsidian://podnotes change.
  • Offline, opening the view can still request feeds in the background; that is Opening the view offline still requests every feed #460.

Deviations from the design

  1. Search and Hide played live only on a podcast's page, as the journey map has them (J-LIST-02, J-LIST-03) and the prototype's list pages show no filter bar. Latest and Played show every episode, with played ones muted.
  2. Offline, podcast search stays typeable and answers "Search needs a connection" without a request, as the spec's first-run row says, rather than a disabled field (the component inventory).
  3. At sidebar and phone widths a queue row puts the podcast name on its own line, by SPEC 10's rule for every row; the prototype's queue screen keeps it beside the time.
  4. The Offline strip shows on every page, not only on list pages, so every disabled refresh has its reason above it.
  5. Obsidian's public Menu API has no submenu, so Add to playlist opens a second menu.
  6. Local files show the file's basename without its extension, and no duration until the file has played once.
  7. Clear queue's Undo restores the queue only while it is still empty, so it never overwrites what was queued since.
  8. Four new Notices explain a refused feed address: "Setting a feed address needs a connection.", "You already follow {title} at that address.", "{title} was removed while its address was edited." and "{title} changed while its address was edited. Try again."
  9. The queue menu says "Queue episodes I switch away from", as SPEC 9 does; the Settings tab keeps its own label (UI3).
  10. New playlists get the default icon, because the spec's New playlist prompt asks only for a name and the old icon picker is gone. Existing playlists keep their icons.

Tradeoffs

  • Undo after Remove podcast refetches the feed, because the feed cache drops an unsubscribed feed.

Blast Radius

Every user sees the new list pages, and anyone who managed playlists in Settings now does it in the view. Desktop users who closed the view no longer see it reopen on every start. The bundle shrinks by deleting the legacy components, including the 600-name icon list only the old playlist manager loaded.

Verification

  • Checks at the head: lint (Svelte policies 0 errors, 0 warnings), format:check, typecheck, build, test (svelte-check 0 errors and 0 warnings in both projects; 2,395 unit tests), test:tooling (13 tests) and docs:build all pass.
  • test:e2e passes 136 of 136 on a fresh vault in Obsidian 1.14.4 (Linux). New journeys cover drag and arrow-key reorder (also while playback changes the queue), one data.json write per drag (counted with the write recorder), the queue advancing after a reorder, New playlist from the episode menu, Undo after Remove podcast for a public and a private feed, a plugin reload ending a pending Undo, offline grouping, playing a download offline and the disabled controls, the Reorder Queue command, a drag that leaves the list, and desktop opening the view once on a new install and not on the next start. Status bar covers mini-player playback control after reload #453 was reproduced on master first: the reload journey now asserts the play/pause control ends above the status bar, which fails on master and passes here. Every new unit test and journey failed on a defect planted on purpose, or on the previous head, before it was trusted.
  • Size: 98,239 gzip against master's 99,637 (-1,398), from deleting what the pages replace. Two models, claude-fable-5-1 and gpt-6-astra, reviewed the change in three rounds; an independent verifier checked it live, and its four findings are fixed here (Undo order after several removals, Undo after a plugin reload, one write per drag, a status bar hidden with visibility). A live pass in Obsidian checked every page at 320, 820 and 390 px in light and dark.

Screenshots from real Obsidian 1.14.4. Each sheet has a row per page (Queue, Playlists, a playlist, Latest, Played, Local files, Offline) with master, this PR and the design prototype side by side.

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

#453, with "9 properties" in the status bar (master above, this PR below), at 820 and 320:
Status bar, pane light
Status bar, pane dark
Status bar, sidebar light
Status bar, sidebar dark

New playlist and the Undo notice:
New playlist
Undo after Remove podcast

Dragging a queue row with the mouse: screen recording

Docs at release

  • playlists.md: playlists are created on the Playlists page (New playlist, or Add to playlist > New playlist) and deleted from a playlist's menu with Undo; the Settings section is gone. A new playlist gets the default icon; there is no icon picker any more. Reorder the queue by dragging a row's grip or with Up and Down on the focused grip.
  • commands.md: Reorder Queue opens the Queue page.
  • settings.md: drop the Playlists section.
  • podcasts.md: the podcast menu's Copy feed URL and Edit feed URL; Undo after Remove podcast.
  • local_files.md: the Local files page's plus button, file tiles and folders.
  • faq.md and a new offline section: what plays offline ("Available offline"), and that refresh, search and transcribe wait for a connection.
  • getting-started.md and mobile.md: the view opens once on a new install, on desktop and mobile, then keeps your layout.
  • docs/scripts/screenshots.mjs: the list pages changed; regenerate their screenshots.

Fixes #453.

Note

Add Queue, Playlists, Local files, and list pages with offline mode

  • Adds dedicated pages: QueuePage.svelte with drag and keyboard reordering, PlaylistsPage.svelte with create/delete and Undo, ListPage.svelte for Latest/Played/Favorites/playlists, and a keyed feed view in EpisodesPage.svelte
  • Adds offline support via onlineStatus in online.ts: episodes without a downloaded copy are grouped under Needs a connection, refresh and address-editing are disabled offline, and offline playback attempts are refused before reaching the player
  • Expands library actions in library.ts: removePodcast and deletePlaylist gain position-preserving Undo, setFeedAddress supports public feeds with duplicate-address rejection, and queue.reorder replaces the removed queue.move API
  • Adds the askText prompt modal and a fragment-based Undo notice that is dismissed on plugin unload
  • Risk: breaking changes — queue.move is removed in favor of queue.reorder (see store/index.ts), episode menus no longer offer queue-reordering items, and IPodNotes implementers must now expose online and setAutoQueue (see IPodNotes.ts)

Macroscope summarized ea36929.

Desktop PodNotes no longer opens its view on every start that finds no
PodNotes leaf. On desktop and mobile alike, the view opens once, when
PodNotes starts with no data.json, and later starts keep the layout the
user left. The layout-ready retry goes with the desktop branch:
workspace.onLayoutReady only calls back once the layout is ready.

J-ONB-01 now starts from a real first enable on desktop, and a new
journey checks that a view closed after it stays closed across a
restart until the ribbon icon opens it.
…dal and playlist manager

The queue reorders by placing one entry before or after another, found by key when
the move applies, so a queue that changed since cannot move the wrong episode. The
Reorder Queue command opens the Queue page. The queue's menu move items, the reorder
modal, the settings playlist manager and its icon picker are gone; the Playlists page
lists playlists as rows. A shared text prompt replaces the feed address modal.
… pages

Latest episodes, Played, a playlist and Local files render through one list page
with no filter bar; search, Hide played and Refresh episodes stay on the podcast
page. Cover rows name their source on the same line in the pane and on a line of
its own in the sidebar and on a phone. A vault file shows a file tile, its folder,
and the length its Played record learned. The head shows the queue's time left and
a playlist's count, and Latest gets Refresh all feeds.
…line states

screens.mjs gains the queue, playlists, playlist, latest, played,
local-files and offline states. Each opens its page by page JSON, so the
same states capture master now and the rebuilt list pages later.

Scenes are now ingredient lists. lists adds Favorites, the prototype's
playlists and a restored, paused episode; local writes four silent media
files of the prototype's lengths into vault folders and registers them as
playLocalFile does; offline adds downloads pointing at real vault files.

offline on|off puts the renderer offline through CDP and stops the fixture
server, because requestUrl runs in Obsidian's main process, which CDP does
not reach. It fails unless navigator.onLine, the window event and a saved
feed request all agree. The fixture server lets artwork be cached, so
covers still show offline.

shot hides Obsidian's status bar, which covered the view's bottom-right
corner, and refuses to write a shot whose theme is not the one requested.
…tatus bar

The reload journey opens a note with nine properties in the main area,
so the core Properties plugin shows "9 properties" in Obsidian's status
bar, which floats over the window's bottom-right corner. Before the
mini player's first click and again after the reload, it checks that
the bar spans the play/pause control, that the control ends at or above
the bar's top, and that the control is what its center shows.

screens.mjs gains a status-bar state: the queue with the mini player and
a "9 properties" item in the status bar, which its shots keep and check
lie inside the capture. It has no phone shot, because Obsidian hides
the status bar on mobile.

Refs #453
Obsidian's status bar floats over the window's bottom edge, so with a
status-bar item such as "9 properties" it covered the mini player's
play/pause and capture controls whenever the view sat at the window's
bottom: the main pane, or the right sidebar.

The shell measures how far the bar reaches up into the view
(statusBarInset, from the two rects) and keeps it current with a
ResizeObserver on the view and the bar, which resizes as items come and
go. It follows the view to a popout, which has no status bar, and sets
the result as --pn-status-bar on .podcast-view. The mini player grows by
it as bottom padding, so its background still reaches the pane's edge
and its controls sit above the bar in the compact and wide layouts. With
nothing loaded, the page scroller's bottom padding includes it, and so
does the full player's scroller, so their last rows can scroll clear.
Mobile hides the bar, so its inset is 0 and nothing changes there.

size: 99686 gzip (+274)

Fixes #453
The Queue page lists the queue in its order. Each row has a grip that a pointer drags
and Up and Down move, placing the episode beside the row it crosses, and a button
that removes it. The head shows the queue's time left and a menu with the auto-queue
preference, written through the plugin's one writer, and Clear queue, which offers
Undo while the queue stays empty.
The Playlists page lists Favorites with a heart, then the user's playlists with
their count and total length, then New playlist, which asks for a name and keeps
the rules the settings manager had. Add to playlist always opens, and ends in New
playlist, which creates a playlist holding the episode. A playlist's menu, from its
row or its page's head, deletes it and offers Undo.
The Local files page's head has a plus button that opens the vault media picker.
A vault file's row shows its file tile, its folder and the length its Played record
learned; a PodNotes download listed there stays its podcast's row.
A public podcast's menu copies its feed URL, and a public feed that failed to load
offers Edit feed URL. One feed address prompt serves public and private feeds: a
public feed keeps its key and place, refuses an address another podcast has, and
keeps a pasted private address as a secret. Remove podcast offers Undo, which puts
the exact record back at its place, with a private feed's address.
One online signal, from the window's online and offline events, reaches the view,
the podcast search and the transcribe commands. Offline, an Offline strip heads
every page; Latest, Played, playlists and the podcast page list what plays offline
first and grey what needs a connection; a tap on one says Not downloaded before the
player can queue the episode playing. Refresh, Retry, search, setting a feed address
and transcribing wait for the connection.
… Edit feed URL

A feed address edit captured the saved record before the prompt and compared
only its URL and secret reference after the address loaded, so removing the
podcast and following it again at its old address let the stale edit land on
the new subscription. It also checked the connection only before the prompt.
Now the edit goes ahead only while the record under the key is the object it
started from and the device is online, checked after the prompt and after the
address loads.
A drag placed the episode by comparing only the pointer's height with each
row's band, so dragging sideways into the editor and then down still
reordered the queue, and so did a row scrolled out of sight. The row is now
the one the browser hit-tests at the pointer, so anywhere else places nothing.
…n listeners

The test compared only event names, so removing another function than the
one added still passed. It now captures the listeners the drag added and
requires those same ones removed on unmount.
… online

Offline, the shell's feed check fails every feed due for a refresh, so the
Library's cards wore the failed badge and the podcast page showed "Feed didn't
load" under the Offline strip. While offline they no longer show it, and the
shell checks the feeds again when the connection returns.

The Played and podcast pages skipped their complete load when they opened
offline and never ran it. They now run it once while open and online.

Offline, a Played record that matched no episode may only be in a feed that
has not loaded, so it reads and acts like any episode that is not downloaded:
under Needs a connection, and a tap says Not downloaded. Online it stays
unavailable with the old notice.
openEpisode refuses an episode that cannot play offline before it calls the
player, so any other call of play in src/ui/PodcastView would bypass that
refusal. A guard beside the library-store one scans the view's files for it.
The Queue menu said "Queue episodes I switch from"; SPEC section 9 names the
preference "Queue episodes I switch away from".
…handled

The Reorder Queue command's async callback returned a promise Obsidian never
reads, so a view that failed to open left an unhandled rejection. The command
now chains the reveal and the Queue page and logs a failure as the startup
reveal does.
Obsidian styles .flair as an uppercase, letter-spaced tag with a start
margin. The shell's flair reset its background and padding only, so the
Queue head read "2 H 23 MIN", spaced further from its title than the
design. The flair now also resets the case, letter spacing and margin.
A drag focuses the grip it starts on, so arrow keys can carry on from
where the pointer left the episode. Dragging a row down the list moved
its node past the next row, which took focus from the grip and left it
on the page, so the arrow keys did nothing after the drag. Each move now
returns focus to the grip once the rows settle, as the arrow keys do.
Edit feed URL showed a feed's address in Obsidian's default narrow text
field, so only the end of the address was visible and the host was cut
off. The text prompt's label keeps its width and the field takes the
rest of the row, in Edit feed URL, Set feed address and New playlist.
On a phone, Obsidian already stacks the label over a full-width field.

The Modal mock gains modalEl, as Obsidian's Modal has.
Since the view opens once on a new install on desktop as well, mobile
emulation is no longer needed to see PodNotes stay closed on later
starts. Checked on desktop: one leaf with First run after the new
install, zero leaves after closing it and reloading.
# Conflicts:
#	src/ui/PodcastView/QueuePage.svelte
… a re-follow

Edit feed URL compared the saved record by identity. A sync republishes
data.json and replaces every record, so the address the user typed and the
fetch validated was dropped without a word. Add podcast keeps its search
results, so removing the podcast and following the same result again stored
the very same object, and the old edit landed on the new subscription.

The edit now watches savedFeeds for the podcast leaving the library while it
runs, and still compares the address fields for a sync that changed them. A
refused edit says so. The edit keeps a sync's changes to the rest of the
record.
When a sync saved the address for another podcast while Edit feed URL
loaded it, the edit was refused without a word. It now shows the same
Notice as the check before the load.
…oves to a pane of the same size

Moving the PodNotes leaf from a pane beside Obsidian's status bar to an equal
pane under it resized nothing, so the inset stayed 0 and the status bar covered
the mini player's play/pause. The inset is now measured again on the
workspace's layout-change, which Obsidian sends after a leaf moves.
…oved notice in removePodcast

removePodcast now closes the 7 s "Removed X" Notice by clicking its text, as a
user does, so the Notice cannot cover a later click target. The helpers add a
held-mouse drag with trusted CDP input, pressKey (which replaces pressEscape),
network emulation for offline, a fixture server that drops every request, and
runFromPalette turns on the command palette core plugin it needs.
… then let it advance; New playlist; Reorder Queue

Each journey failed on a planted defect: placing by index, a stale neighbour, no refocus, the wrong side of a row, a drag beside the list, a stale queue advance, an empty playlist name that closes the prompt, and the command navigating before it reveals the view.
…ar sliding open counts too

Opening PodNotes in a collapsed right sidebar measured the inset while the view
sat beyond the window's edge. The sidebar then slid open without resizing the
view, so the status bar covered the mini player's play/pause until something
resized it; the journey "a reload restores the last page and the episode paused
where it stopped, in the mini player" failed on a fresh vault for this. Obsidian
sends resize once a sidebar has opened and after a leaf moves to another pane,
so the inset is measured on resize instead of layout-change.
actions.ts held the target map, trusted input, waiting and reading, and
navigation and lifecycle in 1,030 lines. Each concern now has its own module
and every importer names the module that owns what it imports.
…feed address was edited

One refusal() decides, in order, removed, changed, offline and taken, and each
call site shows its message. Before, three predicates each showed a Notice, and
a removed podcast got the message for a changed one.
…d taps and search once online

J-OFF-01 now seeds Latest so a downloaded episode is not first online, plays a
downloaded episode that is not the current one while offline, and after the
connection returns plays an episode that needed it and runs a search. The feed
fails through the fixture server while online, so the journey no longer waits
on a request PodNotes sends offline.
@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: ea36929
Status: ✅  Deploy successful!
Preview URL: https://cbd7a010.podnotes.pages.dev
Branch Preview URL: https://feat-ui-lists.podnotes.pages.dev

View logs

…ter several removals

Undo placed a podcast or playlist at the index it had when removed, which is stale once another removal is pending. The actions now remember the order with every removed key, and an Undo resolves its place among the keys there now.
A pending Undo outlived a plugin reload, and clicking it ran the old instance's actions: a private feed's address went back into SecretStorage while the podcast never reached data.json. Unload now hides every Undo notice and disarms its button. A notice the pointer keeps up past its seven seconds stays tracked until it is gone.
…on release

Each row a drag crossed wrote the queue, so a slow drag across three rows wrote data.json three times. The drag now holds where it would put its episode, by entry, and the page shows the queue with that placement applied, merged with changes playback makes meanwhile. Release writes one placement; Escape, a cancelled pointer, the episode leaving the queue, or leaving the page write nothing.
…nd measure again when a theme or snippet changes

A status bar hidden with visibility: hidden keeps its box, so the mini player kept 27 px of blank space above it. A snippet that hides the bar resizes nothing, so css-change now reports too.
@chhoumann
chhoumann merged commit 427c9d5 into master Oct 13, 2026
17 checks passed
@chhoumann
chhoumann deleted the feat/ui-lists branch October 13, 2026 20:05
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.

Status bar covers mini-player playback control after reload

1 participant