Repository navigation
feat(ui): add the First run, Add podcast, Library and podcast episodes pages - #447
Merged
Merged
Conversation
screens.mjs places the view at sidebar 320, pane 820 and phone 390, switches theme, seeds a scene and shoots the five UI1a states, with a contact sheet beside the prototype's screen. fixtures.mjs gains a library manifest of eight podcasts, one whose feed answers 404.
The v3 pages change the library only through ShellServices.actions (src/services/library.ts), which carries the rules PodcastQueryGrid and spawnEpisodeContextMenu kept to themselves: private-feed interning, namesake refusal, setting membership instead of toggling it. Episode rows render one pure model (episodeRow.ts), and the episode and podcast menus are built as data that one renderer shows (menus.ts), anchored in the pointer's own document. Head.libraryActions becomes Head.action, so the wide Library gets Refresh all and a podcast's page its menu button. The legacy components still run beside these until the pages replace them. size: 100097 gzip (+1337 before the legacy deletions)
First run is the empty library's page: the mark, "Add your first podcast", a focused "Search or paste a link" field, and quiet buttons to play a vault file or import OPML. Its first keystroke opens Add podcast, whose field sits in the head with the text and the caret kept. Results name each show's author and episode count; a pasted RSS URL or Apple Podcasts link resolves to one result with a chip naming its source. A tap adds the show and opens its episodes; an added show shows a check and opens. Failures read as fixed lines, never an error's text, and only the newest input may publish. Search podcasts leaves the settings tab: PodcastQueryGrid and PodcastResultCard are deleted, their rules live in LibraryActions and PodcastSearch, and their tests moved to AddPodcastPage.test.ts and library.test.ts. The settings tab and First run share pickFile, and the file menu and the vault media picker share playLocalFile. size: 98908 gzip (-1189 against the contracts commit)
… menu The Library's grid is the shell's .pn-grid, sized by the view's own layout classes: 84 px tiles compact, 108 px wide, three columns on a phone, with no square backdrop behind rounded art. A tile whose artwork is missing or fails shows the podcast's title on a plain tile instead of initials or a blank square, and a podcast whose feed did not load wears a badge and says so in its label. Right-click or a long press opens the podcast menu. The wide Library's head refreshes every feed. PodcastGrid and PodcastGridCard are deleted; the Playlists page uses the same grid. size: 98703 gzip (-205)
A podcast's page is headed by its artwork, title and author. A feed that did not load shows a "Feed didn't load" banner naming its server, never the address or the error, and keeps the cached episodes under it; Retry reloads, and a private feed whose address is not on this device offers "Set feed address" instead. The filter bar searches, hides played episodes and refreshes. Rows show played, in progress with the time left, playing or paused, unavailable, downloaded and has note, and say it in their label. Titles clamp at two lines and the More button sits beside them, never over them (#371). Tapping a row plays it and opens the player; the current episode only opens the player. More, right-click and long press open the episode menu. Latest, Queue, Local files, Played and a playlist render through the same page and row. The player's queue uses the row and the menus too. EpisodeList, EpisodeListItem, EpisodeListHeader, spawnEpisodeContextMenu and ImageLoader are deleted, with the shell's legacy scroller overrides; the libraryContext guard now allows only MainView. size: 98171 gzip (-532)
… time A new install, a start that found no data.json, opens the view in the mobile app as it already does on desktop, so the empty view's search is the first thing a new user meets (#346). PodNotes writes data.json on that start, so later mobile starts stay dormant as before. size: 98200 gzip (+29)
The episode row decides "downloaded" from the library it is given, and an unavailable Played record says so in its row's label, so downloadedEpisodes.isEpisodeDownloaded and EpisodeListEntry's unavailableReason have no reader left. size: 98167 gzip (-33)
# Conflicts: # .claude/skills/verify/SKILL.md # .claude/skills/verify/fixtures.mjs
The mini player and episode rows say time left the same way, from one function. Two comments that restated their code go. size: 98177 gzip (+10)
Episode rows ask whether their episode has a note, and since #442 that lookup reads every note's frontmatter for its guid. A podcast's page asks for all its rows at once, which read the vault once per episode. The notes with a guid are now read once per task and read again in the next, so a note created or synced since is still found. size: 99155 gzip (+40)
Obsidian's button style sets white-space: nowrap, which episode rows, search results and grid cards inherited, so a long title stayed on one clipped line instead of taking the two-line clamp. Live at 320 px the #213 Field Notes title now takes two lines ending in an ellipsis beside More options. npm run size: 99,109 gzip (master e3243c4: 99,768).
The banner names the failure, keeps the cached episodes and offers Retry or Set feed address, so the podcast page no longer also raises the old "Could not load episodes" and private-address Notices.
From the rail the page has nothing before it and stays without Back.
…ecret An episode opened through an obsidian://podnotes URI for a private feed that is not subscribed carries the feed's own credential-bearing address, which the episode link embeds. The menu now offers no link for such an episode. A subscribed private feed's link keeps its placeholder, and the URI format is unchanged. npm run size: 99,118 gzip (master e3243c4: 99,768).
Two episodes of one feed that share a title share a key, so after one played, tapping the other only opened the player and both rows showed as playing. A row, Play and Open player now count an episode as current only when it has the player's key and the same media, as the player's own reuse rule does. The queue keeps its key identity. npm run size: 99,135 gzip (master e3243c4: 99,768).
…y feed The placeholder for such a record has no media or link, so Create note and Add to Favorites made a note and a favorite of nothing playable. As on master, its menu now holds only Mark as played or unplayed. Leaving early for it also drops the menu's other availability checks. npm run size: 99,135 gzip (master e3243c4: 99,768).
Each row asked getPodcastNote, whose title-overlap scan reads the whole notes folder for every row without a note, on mount and on each filter keystroke, and the icon never changed after a note was created or deleted. Rows now ask findExactPodcastNote, the guid index and the template's paths only, through a lookup the view replaces when a note is created, deleted or renamed. The view registers those vault events once the layout is ready, since the vault reports every file as created while it loads, and Obsidian removes them when the view closes. Edits do not refresh rows. The menu and opening a note keep the full lookup. npm run size: 99,219 gzip (master e3243c4: 99,768).
…d podcast pages Podcasts are added on the view's Add podcast page and removed from the podcast menu, Set feed address lives in the podcast page's banner and menu, and the episode menu uses the new labels. A journey that waited on the old Updating banner waits on feed status instead, the offline failure is asserted through the banner, the Library scroll journey has enough podcasts to scroll the denser grid, and the restored current episode's row opens the player, where play resumes it.
…eed error with Retry The fixture server can fail a feed until it is restored, and the iTunes stub answers lookups, so an Apple Podcasts link resolves offline.
J-ONB-02 splits into a true new install, with no data.json and no PodNotes leaf, and a later mobile start, which stays closed.
A percentage padding resolves against the containing block, so a 44 px result cover in a 500 px row took about 48 px of padding a side, grew to 97 px and lost its title. The padding is now 10cqw on the title, the cover being its inline-size container, and the title sits left as in the prototype. Rendered in headless Chrome with Obsidian 1.14.4's app.css at a 500 px view: grid card 94x94, result row 44x44, hero 64x64 and episode row 40x40, each showing its title (before: 97x97, 100x100 and 95x95 with a 0x0 title). The title is now 12% of the cover, near the prototype's 12.5cqw, so a grid card's reads 11.2 px, not 9 px. npm run size: 99,221 gzip (master e3243c4: 99,768).
…all recipe Adding a podcast happens on the view's Add podcast page, removing it in the podcast menu, and Set feed address in the podcast page's banner or menu. The New install recipe disables PodNotes, deletes its data.json and enables it again, optionally under mobile emulation.
The Played page filtered its own order by the search's results, so a search for "reading" listed a newer "Reading slowly, again and again" above "Reading". It now lists the search's results in their order, as master's filterPlayedEntries did. npm run size: 99,237 gzip (master e3243c4: 99,768).
Master's queue in the full player followed the hide played preference, as every list did. The rows that replaced it listed every queued episode. The queue now reads the preference from the library context. npm run size: 99,257 gzip (master e3243c4: 99,768).
The test rendered the player over untyped services with no actions and only looked for the item's title, so it passed with a reorder that did nothing. It now uses the test services, chooses Move up in queue and checks the queue's order. npm run size: 99,257 gzip (master e3243c4: 99,768); tests only.
… Episodes stays open
Keeps only the whys the code cannot show: security, races, platform behavior and interface contracts.
…e result's episode count size: 99,317 gzip
Deploying podnotes with
|
| Latest commit: |
1c7ac0a
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://a95f0221.podnotes.pages.dev |
| Branch Preview URL: | https://feat-ui-pages.podnotes.pages.dev |
This was referenced Oct 13, 2026
… copy A row and its menu matched a download by podcast and title, so a same-titled episode of the same podcast showed the other's download, and its Remove download would delete that file, while playback streamed it. The row, the menu and playback now share one matcher, downloadedCopy.
…h setting changes
…ts at it Rows looked their notes up again only for vault changes, so after a change to Note creation file path a row kept its old state while its menu offered Open note. The field now announces the change through the plugin store, as the queue toggle does, and the view looks again when the note path changes. The deferred-view test now opens and closes its view through load and unload, as Obsidian does.
On a new mobile install the view mounted inside a hidden leaf, so the field's focus call failed and typing did nothing until a tap. An autofocus field now takes focus the first time it has a width, once.
Typing came right after First run showed, so it could reach the window before the field took focus on the next frame. The journeys now wait until the field has focus, as a user waits for the caret, and fail by name when it never does.
Brings in 25ca793, test(e2e): retain runtime errors and recover interrupted runs (#446), and keeps both sides of every conflict: - journey/actions.ts: the v3 selectors (.pn-head back button, button.pn-gridcard with its failed label, .pn-ep-main/.pn-ep-title, button.pn-result) built through #446's jsLiteral and byAttribute. ui.removePodcast stays deleted; #447 removes podcasts through the podcast menu. failedPodcastCard and shownResults referenced the q alias #446 deleted, so they use byAttribute and jsLiteral. - journey/fixtures.ts: the iTunes stub's search and lookup handler, with the results literal written by jsLiteral. - journey-feeds.test.ts: the empty feed shows no feed error banner and raises no new Notice. - journey-mobile.test.ts: the new-install test checks runtime errors with assertNoNewRuntimeErrors, #446's replacement for assertNoRuntimeErrors. - local-files-runtime and marketplace-review-runtime: .pn-ep-title with jsLiteral values. - private-feeds.test.ts: the Library menu path for Set feed address; the settings-tab path stays removed.
The head's Back is .pn-back, or .pn-iconbtn on the Add podcast page, and the harness probe sits outside the head; the label alone names it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
An empty PodNotes view said "No saved podcasts." and searching for a show was only possible in Settings. This was the most common complaint in user feedback (#346). This PR builds the v3 First run, Add podcast, Library and Podcast episodes pages from the design spec and prototype, and deletes the legacy grid, episode list and settings search they replace.
What changed
Scope
obsidian://podnoteschange. Desktop still opens the view at start when no leaf exists, as before.Tradeoffs
LibraryActionsservice (src/services/library.ts). It covers adding and removing a podcast, setting a private feed's address, queue membership and order, Favorites, playlists, and played state. Downloads, OPML import, local files, the hide-played preference and settings writes keep their existing owners. View components read library stores only by context, and the guard test now allows onlyindex.ts. Exposing the store objects instead would have moved the identity rules into every menu.Menu.Blast Radius
Every user sees the new pages, and Search Podcasts leaves Settings. The private-feed rules moved from the settings search unchanged, with their tests: interning into SecretStorage, namesake refusal, last-reference secret cleanup, and the sync-race guard. The bundle shrinks from 99,768 to 99,412 bytes gzip (-356) against master e3243c4, by deleting the components it replaces.
Verification
Sidebar 320, light and dark:


Pane 820, light and dark:


Phone 390 (emulated), light and dark:


New install with mobile emulation: no view and no data.json, enable PodNotes, the view opens on First run, paste a feed URL, tap the result, tap an episode, and it plays. A reload afterwards keeps the view closed.

screen recording
Docs at release
Closes #346. Refs #344, #371.
Note
Add First run, Add podcast, Library, and Episodes pages to PodNotes UI
EpisodeRow,Cover,SearchField, and page components (PodcastView.svelte, AddPodcastPage.svelte, LibraryPage.svelte, EpisodesPage.svelte)PodcastSearchservice that accepts free text, Apple Podcasts URLs, and HTTP/HTTPS feed URLs, with debounced directory search, stale-result suppression, and categorized failures (podcastSearch.ts)createLibraryActionsin library.ts for adding, removing, and setting addresses for podcasts, with private-feed secret handling and idempotent queue/favorite/played stateDirectoryShowwith feed metadata plusepisodeCountfromtrackCount(iTunesAPIConsumer.ts)EpisodeListEntry.unavailableReasonis removed;IPodNotesimplementations must providefeedUrlsMacroscope summarized 1c7ac0a.